diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index a48fba892b..effff6a27a 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -3,12 +3,13 @@ "alias": "buttons", "title": "Buttons", "image": "tb-image:YnV0dG9ucy5zdmc=:IkJ1dHRvbnMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzUiIHk9IjEyLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjAuNzUiIHk9IjEyLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIHN0cm9rZT0iIzNGNTJERCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPHBhdGggZD0iTTYyLjE2NjMgNTEuMzMzM1Y0NC4zMzMzSDY2LjgzM1Y1MS4zMzMzSDcyLjY2NjNWNDJINzYuMTY2M0w2NC40OTk3IDMxLjVMNTIuODMzIDQySDU2LjMzM1Y1MS4zMzMzSDYyLjE2NjNaIiBmaWxsPSIjM0Y1MkREIi8+CjxwYXRoIGQ9Ik05MC4xOTUzIDQyLjkzMTZIODYuMjFMODYuMTg4NSA0MC45NjU4SDg5LjY2ODlDOTAuMjU2MiA0MC45NjU4IDkwLjc1MzkgNDAuODc5OSA5MS4xNjIxIDQwLjcwOEM5MS41Nzc1IDQwLjUyOSA5MS44OTI2IDQwLjI3NDcgOTIuMTA3NCAzOS45NDUzQzkyLjMyMjMgMzkuNjA4NyA5Mi40Mjk3IDM5LjIwNDEgOTIuNDI5NyAzOC43MzE0QzkyLjQyOTcgMzguMjA4NyA5Mi4zMjk0IDM3Ljc4MjYgOTIuMTI4OSAzNy40NTMxQzkxLjkyODQgMzcuMTIzNyA5MS42MjA0IDM2Ljg4MzggOTEuMjA1MSAzNi43MzM0QzkwLjc5NjkgMzYuNTgzIDkwLjI3NDEgMzYuNTA3OCA4OS42MzY3IDM2LjUwNzhIODcuMDI2NFY1MEg4NC4zMzAxVjM0LjM1OTRIODkuNjM2N0M5MC40OTYxIDM0LjM1OTQgOTEuMjYyNCAzNC40NDE3IDkxLjkzNTUgMzQuNjA2NEM5Mi42MTU5IDM0Ljc3MTIgOTMuMTkyNCAzNS4wMjkgOTMuNjY1IDM1LjM3OTlDOTQuMTQ0OSAzNS43MjM2IDk0LjUwNjUgMzYuMTYwNSA5NC43NSAzNi42OTA0Qzk1LjAwMDcgMzcuMjIwNCA5NS4xMjYgMzcuODUwNiA5NS4xMjYgMzguNTgxMUM5NS4xMjYgMzkuMjI1NiA5NC45NzIgMzkuODE2NCA5NC42NjQxIDQwLjM1MzVDOTQuMzU2MSA0MC44ODM1IDkzLjkwMTQgNDEuMzE2NyA5My4yOTk4IDQxLjY1MzNDOTIuNjk4MiA0MS45ODk5IDkxLjk0OTkgNDIuMTkwNCA5MS4wNTQ3IDQyLjI1NDlMOTAuMTk1MyA0Mi45MzE2Wk05MC4wNzcxIDUwSDg1LjM2MTNMODYuNTc1MiA0Ny44NjIzSDkwLjA3NzFDOTAuNjg1OSA0Ny44NjIzIDkxLjE5NDMgNDcuNzYyIDkxLjYwMjUgNDcuNTYxNUM5Mi4wMTA3IDQ3LjM1MzggOTIuMzE1MSA0Ny4wNzEgOTIuNTE1NiA0Ni43MTI5QzkyLjcyMzMgNDYuMzQ3NyA5Mi44MjcxIDQ1LjkyMTUgOTIuODI3MSA0NS40MzQ2QzkyLjgyNzEgNDQuOTI2MSA5Mi43Mzc2IDQ0LjQ4NTcgOTIuNTU4NiA0NC4xMTMzQzkyLjM3OTYgNDMuNzMzNyA5Mi4wOTY3IDQzLjQ0MzcgOTEuNzEgNDMuMjQzMkM5MS4zMjMyIDQzLjAzNTUgOTAuODE4NCA0Mi45MzE2IDkwLjE5NTMgNDIuOTMxNkg4Ny4xNjZMODcuMTg3NSA0MC45NjU4SDkxLjEyOTlMOTEuNzQyMiA0MS43MDdDOTIuNjAxNiA0MS43MzU3IDkzLjMwNyA0MS45MjU1IDkzLjg1ODQgNDIuMjc2NEM5NC40MTcgNDIuNjI3MyA5NC44MzI0IDQzLjA4MiA5NS4xMDQ1IDQzLjY0MDZDOTUuMzc2NiA0NC4xOTkyIDk1LjUxMjcgNDQuODAwOCA5NS41MTI3IDQ1LjQ0NTNDOTUuNTEyNyA0Ni40NDA4IDk1LjI5NDMgNDcuMjc1MSA5NC44NTc0IDQ3Ljk0ODJDOTQuNDI3NyA0OC42MjE0IDkzLjgwODMgNDkuMTMzNSA5Mi45OTkgNDkuNDg0NEM5Mi4xODk4IDQ5LjgyODEgOTEuMjE1OCA1MCA5MC4wNzcxIDUwWk0xMDUuMjE2IDQ3LjI2MDdWMzguMzc3SDEwNy44MTVWNTBIMTA1LjM2NkwxMDUuMjE2IDQ3LjI2MDdaTTEwNS41ODEgNDQuODQzOEwxMDYuNDUxIDQ0LjgyMjNDMTA2LjQ1MSA0NS42MDI5IDEwNi4zNjUgNDYuMzIyNiAxMDYuMTkzIDQ2Ljk4MTRDMTA2LjAyMSA0Ny42MzMxIDEwNS43NTcgNDguMjAyNSAxMDUuMzk4IDQ4LjY4OTVDMTA1LjA0IDQ5LjE2OTMgMTA0LjU4MiA0OS41NDUyIDEwNC4wMjMgNDkuODE3NEMxMDMuNDY1IDUwLjA4MjQgMTAyLjc5NSA1MC4yMTQ4IDEwMi4wMTUgNTAuMjE0OEMxMDEuNDQ5IDUwLjIxNDggMTAwLjkzIDUwLjEzMjUgMTAwLjQ1NyA0OS45Njc4Qzk5Ljk4NDQgNDkuODAzMSA5OS41NzYyIDQ5LjU0ODggOTkuMjMyNCA0OS4yMDUxQzk4Ljg5NTggNDguODYxMyA5OC42MzQ0IDQ4LjQxMzcgOTguNDQ4MiA0Ny44NjIzQzk4LjI2MiA0Ny4zMTA5IDk4LjE2ODkgNDYuNjUyIDk4LjE2ODkgNDUuODg1N1YzOC4zNzdIMTAwLjc1OFY0NS45MDcyQzEwMC43NTggNDYuMzI5OCAxMDAuODA4IDQ2LjY4NDIgMTAwLjkwOCA0Ni45NzA3QzEwMS4wMDggNDcuMjUgMTAxLjE0NSA0Ny40NzU2IDEwMS4zMTYgNDcuNjQ3NUMxMDEuNDg4IDQ3LjgxOTMgMTAxLjY4OSA0Ny45NDExIDEwMS45MTggNDguMDEyN0MxMDIuMTQ3IDQ4LjA4NDMgMTAyLjM5MSA0OC4xMjAxIDEwMi42NDggNDguMTIwMUMxMDMuMzg2IDQ4LjEyMDEgMTAzLjk2NiA0Ny45NzY5IDEwNC4zODkgNDcuNjkwNEMxMDQuODE4IDQ3LjM5NjggMTA1LjEyMyA0Ny4wMDI5IDEwNS4zMDIgNDYuNTA4OEMxMDUuNDg4IDQ2LjAxNDYgMTA1LjU4MSA0NS40NTk2IDEwNS41ODEgNDQuODQzOFpNMTE2LjA0NyAzOC4zNzdWNDAuMjY3NkgxMDkuNDk0VjM4LjM3N0gxMTYuMDQ3Wk0xMTEuMzg1IDM1LjUzMDNIMTEzLjk3NFY0Ni43ODgxQzExMy45NzQgNDcuMTQ2MiAxMTQuMDI0IDQ3LjQyMTkgMTE0LjEyNCA0Ny42MTUyQzExNC4yMzEgNDcuODAxNCAxMTQuMzc4IDQ3LjkyNjggMTE0LjU2NCA0Ny45OTEyQzExNC43NTEgNDguMDU1NyAxMTQuOTY5IDQ4LjA4NzkgMTE1LjIyIDQ4LjA4NzlDMTE1LjM5OSA0OC4wODc5IDExNS41NzEgNDguMDc3MSAxMTUuNzM1IDQ4LjA1NTdDMTE1LjkgNDguMDM0MiAxMTYuMDMzIDQ4LjAxMjcgMTE2LjEzMyA0Ny45OTEyTDExNi4xNDQgNDkuOTY3OEMxMTUuOTI5IDUwLjAzMjIgMTE1LjY3OCA1MC4wODk1IDExNS4zOTIgNTAuMTM5NkMxMTUuMTEyIDUwLjE4OTggMTE0Ljc5IDUwLjIxNDggMTE0LjQyNSA1MC4yMTQ4QzExMy44MyA1MC4yMTQ4IDExMy4zMDQgNTAuMTExIDExMi44NDYgNDkuOTAzM0MxMTIuMzg3IDQ5LjY4ODUgMTEyLjAyOSA0OS4zNDExIDExMS43NzEgNDguODYxM0MxMTEuNTE0IDQ4LjM4MTUgMTExLjM4NSA0Ny43NDQxIDExMS4zODUgNDYuOTQ5MlYzNS41MzAzWk0xMjMuNjIzIDM4LjM3N1Y0MC4yNjc2SDExNy4wN1YzOC4zNzdIMTIzLjYyM1pNMTE4Ljk2MSAzNS41MzAzSDEyMS41NVY0Ni43ODgxQzEyMS41NSA0Ny4xNDYyIDEyMS42IDQ3LjQyMTkgMTIxLjcgNDcuNjE1MkMxMjEuODA4IDQ3LjgwMTQgMTIxLjk1NCA0Ny45MjY4IDEyMi4xNDEgNDcuOTkxMkMxMjIuMzI3IDQ4LjA1NTcgMTIyLjU0NSA0OC4wODc5IDEyMi43OTYgNDguMDg3OUMxMjIuOTc1IDQ4LjA4NzkgMTIzLjE0NyA0OC4wNzcxIDEyMy4zMTIgNDguMDU1N0MxMjMuNDc2IDQ4LjAzNDIgMTIzLjYwOSA0OC4wMTI3IDEyMy43MDkgNDcuOTkxMkwxMjMuNzIgNDkuOTY3OEMxMjMuNTA1IDUwLjAzMjIgMTIzLjI1NCA1MC4wODk1IDEyMi45NjggNTAuMTM5NkMxMjIuNjg4IDUwLjE4OTggMTIyLjM2NiA1MC4yMTQ4IDEyMi4wMDEgNTAuMjE0OEMxMjEuNDA3IDUwLjIxNDggMTIwLjg4IDUwLjExMSAxMjAuNDIyIDQ5LjkwMzNDMTE5Ljk2NCA0OS42ODg1IDExOS42MDUgNDkuMzQxMSAxMTkuMzQ4IDQ4Ljg2MTNDMTE5LjA5IDQ4LjM4MTUgMTE4Ljk2MSA0Ny43NDQxIDExOC45NjEgNDYuOTQ5MlYzNS41MzAzWk0xMjUuMTE5IDQ0LjMxNzRWNDQuMDcwM0MxMjUuMTE5IDQzLjIzMjQgMTI1LjI0MSA0Mi40NTU0IDEyNS40ODQgNDEuNzM5M0MxMjUuNzI4IDQxLjAxNiAxMjYuMDc5IDQwLjM4OTMgMTI2LjUzNyAzOS44NTk0QzEyNy4wMDMgMzkuMzIyMyAxMjcuNTY4IDM4LjkwNjkgMTI4LjIzNCAzOC42MTMzQzEyOC45MDggMzguMzEyNSAxMjkuNjY3IDM4LjE2MjEgMTMwLjUxMiAzOC4xNjIxQzEzMS4zNjQgMzguMTYyMSAxMzIuMTIzIDM4LjMxMjUgMTMyLjc4OSAzOC42MTMzQzEzMy40NjIgMzguOTA2OSAxMzQuMDMyIDM5LjMyMjMgMTM0LjQ5NyAzOS44NTk0QzEzNC45NjMgNDAuMzg5MyAxMzUuMzE3IDQxLjAxNiAxMzUuNTYxIDQxLjczOTNDMTM1LjgwNCA0Mi40NTU0IDEzNS45MjYgNDMuMjMyNCAxMzUuOTI2IDQ0LjA3MDNWNDQuMzE3NEMxMzUuOTI2IDQ1LjE1NTMgMTM1LjgwNCA0NS45MzIzIDEzNS41NjEgNDYuNjQ4NEMxMzUuMzE3IDQ3LjM2NDYgMTM0Ljk2MyA0Ny45OTEyIDEzNC40OTcgNDguNTI4M0MxMzQuMDMyIDQ5LjA1ODMgMTMzLjQ2NiA0OS40NzM2IDEzMi44IDQ5Ljc3NDRDMTMyLjEzNCA1MC4wNjggMTMxLjM3OCA1MC4yMTQ4IDEzMC41MzMgNTAuMjE0OEMxMjkuNjgxIDUwLjIxNDggMTI4LjkxOCA1MC4wNjggMTI4LjI0NSA0OS43NzQ0QzEyNy41NzkgNDkuNDczNiAxMjcuMDEzIDQ5LjA1ODMgMTI2LjU0OCA0OC41MjgzQzEyNi4wODIgNDcuOTkxMiAxMjUuNzI4IDQ3LjM2NDYgMTI1LjQ4NCA0Ni42NDg0QzEyNS4yNDEgNDUuOTMyMyAxMjUuMTE5IDQ1LjE1NTMgMTI1LjExOSA0NC4zMTc0Wk0xMjcuNzA4IDQ0LjA3MDNWNDQuMzE3NEMxMjcuNzA4IDQ0Ljg0MDIgMTI3Ljc2MiA0NS4zMzQzIDEyNy44NjkgNDUuNzk5OEMxMjcuOTc3IDQ2LjI2NTMgMTI4LjE0NSA0Ni42NzM1IDEyOC4zNzQgNDcuMDI0NEMxMjguNjAzIDQ3LjM3NTMgMTI4Ljg5NyA0Ny42NTEgMTI5LjI1NSA0Ny44NTE2QzEyOS42MTMgNDguMDUyMSAxMzAuMDM5IDQ4LjE1MjMgMTMwLjUzMyA0OC4xNTIzQzEzMS4wMTMgNDguMTUyMyAxMzEuNDI4IDQ4LjA1MjEgMTMxLjc3OSA0Ny44NTE2QzEzMi4xMzcgNDcuNjUxIDEzMi40MzEgNDcuMzc1MyAxMzIuNjYgNDcuMDI0NEMxMzIuODg5IDQ2LjY3MzUgMTMzLjA1OCA0Ni4yNjUzIDEzMy4xNjUgNDUuNzk5OEMxMzMuMjggNDUuMzM0MyAxMzMuMzM3IDQ0Ljg0MDIgMTMzLjMzNyA0NC4zMTc0VjQ0LjA3MDNDMTMzLjMzNyA0My41NTQ3IDEzMy4yOCA0My4wNjc3IDEzMy4xNjUgNDIuNjA5NEMxMzMuMDU4IDQyLjE0MzkgMTMyLjg4NiA0MS43MzIxIDEzMi42NDkgNDEuMzc0QzEzMi40MiA0MS4wMTYgMTMyLjEyNyA0MC43MzY3IDEzMS43NjkgNDAuNTM2MUMxMzEuNDE4IDQwLjMyODUgMTMwLjk5OSA0MC4yMjQ2IDEzMC41MTIgNDAuMjI0NkMxMzAuMDI1IDQwLjIyNDYgMTI5LjYwMiA0MC4zMjg1IDEyOS4yNDQgNDAuNTM2MUMxMjguODkzIDQwLjczNjcgMTI4LjYwMyA0MS4wMTYgMTI4LjM3NCA0MS4zNzRDMTI4LjE0NSA0MS43MzIxIDEyNy45NzcgNDIuMTQzOSAxMjcuODY5IDQyLjYwOTRDMTI3Ljc2MiA0My4wNjc3IDEyNy43MDggNDMuNTU0NyAxMjcuNzA4IDQ0LjA3MDNaTTE0MC45MTMgNDAuODU4NFY1MEgxMzguMzI0VjM4LjM3N0gxNDAuNzYzTDE0MC45MTMgNDAuODU4NFpNMTQwLjQ1MSA0My43NTg4TDEzOS42MTMgNDMuNzQ4QzEzOS42MiA0Mi45MjQ1IDEzOS43MzUgNDIuMTY4OSAxMzkuOTU3IDQxLjQ4MTRDMTQwLjE4NiA0MC43OTM5IDE0MC41MDEgNDAuMjAzMSAxNDAuOTAyIDM5LjcwOUMxNDEuMzExIDM5LjIxNDggMTQxLjc5OCAzOC44MzUzIDE0Mi4zNjMgMzguNTcwM0MxNDIuOTI5IDM4LjI5ODIgMTQzLjU1OSAzOC4xNjIxIDE0NC4yNTQgMzguMTYyMUMxNDQuODEyIDM4LjE2MjEgMTQ1LjMxNyAzOC4yNDA5IDE0NS43NjkgMzguMzk4NEMxNDYuMjI3IDM4LjU0ODggMTQ2LjYxNyAzOC43OTU5IDE0Ni45MzkgMzkuMTM5NkMxNDcuMjY5IDM5LjQ4MzQgMTQ3LjUyIDM5LjkzMSAxNDcuNjkxIDQwLjQ4MjRDMTQ3Ljg2MyA0MS4wMjY3IDE0Ny45NDkgNDEuNjk2MyAxNDcuOTQ5IDQyLjQ5MTJWNTBIMTQ1LjM1VjQyLjQ4MDVDMTQ1LjM1IDQxLjkyMTkgMTQ1LjI2NyA0MS40ODE0IDE0NS4xMDMgNDEuMTU5MkMxNDQuOTQ1IDQwLjgyOTggMTQ0LjcxMiA0MC41OTcgMTQ0LjQwNCA0MC40NjA5QzE0NC4xMDQgNDAuMzE3NyAxNDMuNzI4IDQwLjI0NjEgMTQzLjI3NiA0MC4yNDYxQzE0Mi44MzIgNDAuMjQ2MSAxNDIuNDM1IDQwLjMzOTIgMTQyLjA4NCA0MC41MjU0QzE0MS43MzMgNDAuNzExNiAxNDEuNDM2IDQwLjk2NTggMTQxLjE5MiA0MS4yODgxQzE0MC45NTYgNDEuNjEwNCAxNDAuNzczIDQxLjk4MjcgMTQwLjY0NSA0Mi40MDUzQzE0MC41MTYgNDIuODI3OCAxNDAuNDUxIDQzLjI3OSAxNDAuNDUxIDQzLjc1ODhaIiBmaWxsPSIjM0Y1MkREIi8+CjxyZWN0IHg9IjAuNzUiIHk9Ijg4Ljc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjAuNzUiIHk9Ijg4Ljc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIHN0cm9rZT0iIzNGNTJERCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPHBhdGggZD0iTTY1LjQ5OTcgMTExVjExMy4zMzNINzUuNTIxM0w2NC4zMzMgMTI0LjUyMkw2NS45NzggMTI2LjE2N0w3Ny4xNjYzIDExNC45NzhWMTI1SDc5LjQ5OTdWMTExSDY1LjQ5OTdaIiBmaWxsPSIjM0Y1MkREIi8+CjxwYXRoIGQ9Ik0xMDAuNTY0IDEyMS45NzJDMTAwLjU2NCAxMjEuNjQ5IDEwMC41MTQgMTIxLjM2MyAxMDAuNDE0IDEyMS4xMTJDMTAwLjMyMSAxMjAuODYyIDEwMC4xNTMgMTIwLjYzMiA5OS45MDkyIDEyMC40MjVDOTkuNjY1NyAxMjAuMjE3IDk5LjMyMTkgMTIwLjAxNyA5OC44Nzc5IDExOS44MjNDOTguNDQxMSAxMTkuNjIzIDk3Ljg4MjUgMTE5LjQxOSA5Ny4yMDIxIDExOS4yMTFDOTYuNDU3NCAxMTguOTgyIDk1Ljc2OTkgMTE4LjcyOCA5NS4xMzk2IDExOC40NDhDOTQuNTE2NiAxMTguMTYyIDkzLjk3MjMgMTE3LjgzMiA5My41MDY4IDExNy40NkM5My4wNDEzIDExNy4wOCA5Mi42Nzk3IDExNi42NDcgOTIuNDIxOSAxMTYuMTZDOTIuMTY0MSAxMTUuNjY2IDkyLjAzNTIgMTE1LjA5NyA5Mi4wMzUyIDExNC40NTJDOTIuMDM1MiAxMTMuODE1IDkyLjE2NzYgMTEzLjIzNSA5Mi40MzI2IDExMi43MTJDOTIuNzA0OCAxMTIuMTg5IDkzLjA4NzkgMTExLjczOCA5My41ODIgMTExLjM1OEM5NC4wODMzIDExMC45NzIgOTQuNjc0MiAxMTAuNjc0IDk1LjM1NDUgMTEwLjQ2N0M5Ni4wMzQ4IDExMC4yNTIgOTYuNzg2OCAxMTAuMTQ1IDk3LjYxMDQgMTEwLjE0NUM5OC43NzA1IDExMC4xNDUgOTkuNzY5NSAxMTAuMzU5IDEwMC42MDcgMTEwLjc4OUMxMDEuNDUyIDExMS4yMTkgMTAyLjEwMSAxMTEuNzk1IDEwMi41NTIgMTEyLjUxOUMxMDMuMDEgMTEzLjI0MiAxMDMuMjM5IDExNC4wNCAxMDMuMjM5IDExNC45MTRIMTAwLjU2NEMxMDAuNTY0IDExNC4zOTggMTAwLjQ1MyAxMTMuOTQ0IDEwMC4yMzEgMTEzLjU1QzEwMC4wMTcgMTEzLjE0OSA5OS42ODcyIDExMi44MzQgOTkuMjQzMiAxMTIuNjA0Qzk4LjgwNjMgMTEyLjM3NSA5OC4yNTEzIDExMi4yNjEgOTcuNTc4MSAxMTIuMjYxQzk2Ljk0MDggMTEyLjI2MSA5Ni40MTA4IDExMi4zNTcgOTUuOTg4MyAxMTIuNTUxQzk1LjU2NTggMTEyLjc0NCA5NS4yNTA3IDExMy4wMDYgOTUuMDQzIDExMy4zMzVDOTQuODM1MyAxMTMuNjY0IDk0LjczMTQgMTE0LjAzNyA5NC43MzE0IDExNC40NTJDOTQuNzMxNCAxMTQuNzQ2IDk0Ljc5OTUgMTE1LjAxNCA5NC45MzU1IDExNS4yNThDOTUuMDcxNiAxMTUuNDk0IDk1LjI3OTMgMTE1LjcxNiA5NS41NTg2IDExNS45MjRDOTUuODM3OSAxMTYuMTI0IDk2LjE4ODggMTE2LjMxNCA5Ni42MTEzIDExNi40OTNDOTcuMDMzOSAxMTYuNjcyIDk3LjUzMTYgMTE2Ljg0NCA5OC4xMDQ1IDExNy4wMDlDOTguOTcxIDExNy4yNjcgOTkuNzI2NiAxMTcuNTUzIDEwMC4zNzEgMTE3Ljg2OEMxMDEuMDE2IDExOC4xNzYgMTAxLjU1MyAxMTguNTI3IDEwMS45ODIgMTE4LjkyMUMxMDIuNDEyIDExOS4zMTUgMTAyLjczNCAxMTkuNzYyIDEwMi45NDkgMTIwLjI2NEMxMDMuMTY0IDEyMC43NTggMTAzLjI3MSAxMjEuMzIgMTAzLjI3MSAxMjEuOTVDMTAzLjI3MSAxMjIuNjA5IDEwMy4xMzkgMTIzLjIwMyAxMDIuODc0IDEyMy43MzNDMTAyLjYwOSAxMjQuMjU2IDEwMi4yMjkgMTI0LjcwNCAxMDEuNzM1IDEyNS4wNzZDMTAxLjI0OCAxMjUuNDQxIDEwMC42NjEgMTI1LjcyNCA5OS45NzM2IDEyNS45MjVDOTkuMjkzMyAxMjYuMTE4IDk4LjUzNDIgMTI2LjIxNSA5Ny42OTYzIDEyNi4yMTVDOTYuOTQ0MyAxMjYuMjE1IDk2LjIwMzEgMTI2LjExNSA5NS40NzI3IDEyNS45MTRDOTQuNzQ5MyAxMjUuNzE0IDk0LjA5MDUgMTI1LjQwOSA5My40OTYxIDEyNS4wMDFDOTIuOTAxNyAxMjQuNTg2IDkyLjQyOSAxMjQuMDcgOTIuMDc4MSAxMjMuNDU0QzkxLjcyNzIgMTIyLjgzMSA5MS41NTE4IDEyMi4xMDQgOTEuNTUxOCAxMjEuMjczSDk0LjI0OEM5NC4yNDggMTIxLjc4MiA5NC4zMzQgMTIyLjIxNSA5NC41MDU5IDEyMi41NzNDOTQuNjg0OSAxMjIuOTMxIDk0LjkzMiAxMjMuMjI1IDk1LjI0NzEgMTIzLjQ1NEM5NS41NjIyIDEyMy42NzYgOTUuOTI3NCAxMjMuODQxIDk2LjM0MjggMTIzLjk0OEM5Ni43NjUzIDEyNC4wNTYgOTcuMjE2NSAxMjQuMTA5IDk3LjY5NjMgMTI0LjEwOUM5OC4zMjY1IDEyNC4xMDkgOTguODUyOSAxMjQuMDIgOTkuMjc1NCAxMjMuODQxQzk5LjcwNTEgMTIzLjY2MiAxMDAuMDI3IDEyMy40MTEgMTAwLjI0MiAxMjMuMDg5QzEwMC40NTcgMTIyLjc2NyAxMDAuNTY0IDEyMi4zOTQgMTAwLjU2NCAxMjEuOTcyWk0xMTAuNzcyIDEyNi4yMTVDMTA5LjkxMyAxMjYuMjE1IDEwOS4xMzYgMTI2LjA3NSAxMDguNDQxIDEyNS43OTZDMTA3Ljc1NCAxMjUuNTA5IDEwNy4xNjcgMTI1LjExMiAxMDYuNjggMTI0LjYwNEMxMDYuMiAxMjQuMDk1IDEwNS44MzEgMTIzLjQ5NyAxMDUuNTczIDEyMi44MUMxMDUuMzE1IDEyMi4xMjIgMTA1LjE4NyAxMjEuMzgxIDEwNS4xODcgMTIwLjU4NlYxMjAuMTU2QzEwNS4xODcgMTE5LjI0NyAxMDUuMzE5IDExOC40MjMgMTA1LjU4NCAxMTcuNjg2QzEwNS44NDkgMTE2Ljk0OCAxMDYuMjE4IDExNi4zMTggMTA2LjY5IDExNS43OTVDMTA3LjE2MyAxMTUuMjY1IDEwNy43MjIgMTE0Ljg2IDEwOC4zNjYgMTE0LjU4MUMxMDkuMDExIDExNC4zMDIgMTA5LjcwOSAxMTQuMTYyIDExMC40NjEgMTE0LjE2MkMxMTEuMjkyIDExNC4xNjIgMTEyLjAxOSAxMTQuMzAyIDExMi42NDIgMTE0LjU4MUMxMTMuMjY1IDExNC44NiAxMTMuNzggMTE1LjI1NCAxMTQuMTg4IDExNS43NjNDMTE0LjYwNCAxMTYuMjY0IDExNC45MTIgMTE2Ljg2MiAxMTUuMTEyIDExNy41NTdDMTE1LjMyIDExOC4yNTEgMTE1LjQyNCAxMTkuMDE4IDExNS40MjQgMTE5Ljg1NVYxMjAuOTYySDEwNi40NDNWMTE5LjEwNEgxMTIuODY3VjExOC44OTlDMTEyLjg1MyAxMTguNDM0IDExMi43NiAxMTcuOTk3IDExMi41ODggMTE3LjU4OUMxMTIuNDIzIDExNy4xODEgMTEyLjE2OSAxMTYuODUxIDExMS44MjUgMTE2LjYwMUMxMTEuNDgxIDExNi4zNSAxMTEuMDIzIDExNi4yMjUgMTEwLjQ1IDExNi4yMjVDMTEwLjAyMSAxMTYuMjI1IDEwOS42MzcgMTE2LjMxOCAxMDkuMzAxIDExNi41MDRDMTA4Ljk3MSAxMTYuNjgzIDEwOC42OTYgMTE2Ljk0NCAxMDguNDc0IDExNy4yODhDMTA4LjI1MiAxMTcuNjMyIDEwOC4wOCAxMTguMDQ3IDEwNy45NTggMTE4LjUzNEMxMDcuODQzIDExOS4wMTQgMTA3Ljc4NiAxMTkuNTU1IDEwNy43ODYgMTIwLjE1NlYxMjAuNTg2QzEwNy43ODYgMTIxLjA5NCAxMDcuODU0IDEyMS41NjcgMTA3Ljk5IDEyMi4wMDRDMTA4LjEzMyAxMjIuNDM0IDEwOC4zNDEgMTIyLjgxIDEwOC42MTMgMTIzLjEzMkMxMDguODg1IDEyMy40NTQgMTA5LjIxNSAxMjMuNzA4IDEwOS42MDIgMTIzLjg5NUMxMDkuOTg4IDEyNC4wNzQgMTEwLjQyOSAxMjQuMTYzIDExMC45MjMgMTI0LjE2M0MxMTEuNTQ2IDEyNC4xNjMgMTEyLjEwMSAxMjQuMDM4IDExMi41ODggMTIzLjc4N0MxMTMuMDc1IDEyMy41MzYgMTEzLjQ5NyAxMjMuMTgyIDExMy44NTUgMTIyLjcyNEwxMTUuMjIgMTI0LjA0NUMxMTQuOTY5IDEyNC40MSAxMTQuNjQzIDEyNC43NjEgMTE0LjI0MiAxMjUuMDk4QzExMy44NDEgMTI1LjQyNyAxMTMuMzUxIDEyNS42OTYgMTEyLjc3MSAxMjUuOTAzQzExMi4xOTggMTI2LjExMSAxMTEuNTMyIDEyNi4yMTUgMTEwLjc3MiAxMjYuMjE1Wk0xMjAuMjYxIDExNi44NThWMTI2SDExNy42NzJWMTE0LjM3N0gxMjAuMTFMMTIwLjI2MSAxMTYuODU4Wk0xMTkuNzk5IDExOS43NTlMMTE4Ljk2MSAxMTkuNzQ4QzExOC45NjggMTE4LjkyNCAxMTkuMDgzIDExOC4xNjkgMTE5LjMwNSAxMTcuNDgxQzExOS41MzQgMTE2Ljc5NCAxMTkuODQ5IDExNi4yMDMgMTIwLjI1IDExNS43MDlDMTIwLjY1OCAxMTUuMjE1IDEyMS4xNDUgMTE0LjgzNSAxMjEuNzExIDExNC41N0MxMjIuMjc3IDExNC4yOTggMTIyLjkwNyAxMTQuMTYyIDEyMy42MDIgMTE0LjE2MkMxMjQuMTYgMTE0LjE2MiAxMjQuNjY1IDExNC4yNDEgMTI1LjExNiAxMTQuMzk4QzEyNS41NzUgMTE0LjU0OSAxMjUuOTY1IDExNC43OTYgMTI2LjI4NyAxMTUuMTRDMTI2LjYxNyAxMTUuNDgzIDEyNi44NjcgMTE1LjkzMSAxMjcuMDM5IDExNi40ODJDMTI3LjIxMSAxMTcuMDI3IDEyNy4yOTcgMTE3LjY5NiAxMjcuMjk3IDExOC40OTFWMTI2SDEyNC42OTdWMTE4LjQ4QzEyNC42OTcgMTE3LjkyMiAxMjQuNjE1IDExNy40ODEgMTI0LjQ1IDExNy4xNTlDMTI0LjI5MyAxMTYuODMgMTI0LjA2IDExNi41OTcgMTIzLjc1MiAxMTYuNDYxQzEyMy40NTEgMTE2LjMxOCAxMjMuMDc1IDExNi4yNDYgMTIyLjYyNCAxMTYuMjQ2QzEyMi4xOCAxMTYuMjQ2IDEyMS43ODMgMTE2LjMzOSAxMjEuNDMyIDExNi41MjVDMTIxLjA4MSAxMTYuNzEyIDEyMC43ODQgMTE2Ljk2NiAxMjAuNTQgMTE3LjI4OEMxMjAuMzA0IDExNy42MSAxMjAuMTIxIDExNy45ODMgMTE5Ljk5MiAxMTguNDA1QzExOS44NjMgMTE4LjgyOCAxMTkuNzk5IDExOS4yNzkgMTE5Ljc5OSAxMTkuNzU5Wk0xMzcuMjc5IDEyMy41OTRWMTA5LjVIMTM5Ljg3OVYxMjZIMTM3LjUyNkwxMzcuMjc5IDEyMy41OTRaTTEyOS43MTcgMTIwLjMxN1YxMjAuMDkyQzEyOS43MTcgMTE5LjIxMSAxMjkuODIxIDExOC40MDkgMTMwLjAyOCAxMTcuNjg2QzEzMC4yMzYgMTE2Ljk1NSAxMzAuNTM3IDExNi4zMjggMTMwLjkzMSAxMTUuODA2QzEzMS4zMjUgMTE1LjI3NiAxMzEuODA0IDExNC44NzEgMTMyLjM3IDExNC41OTJDMTMyLjkzNiAxMTQuMzA1IDEzMy41NzMgMTE0LjE2MiAxMzQuMjgyIDExNC4xNjJDMTM0Ljk4NCAxMTQuMTYyIDEzNS42IDExNC4yOTggMTM2LjEzIDExNC41N0MxMzYuNjYgMTE0Ljg0MiAxMzcuMTExIDExNS4yMzMgMTM3LjQ4MyAxMTUuNzQxQzEzNy44NTYgMTE2LjI0MyAxMzguMTUzIDExNi44NDQgMTM4LjM3NSAxMTcuNTQ2QzEzOC41OTcgMTE4LjI0MSAxMzguNzU1IDExOS4wMTQgMTM4Ljg0OCAxMTkuODY2VjEyMC41ODZDMTM4Ljc1NSAxMjEuNDE3IDEzOC41OTcgMTIyLjE3NiAxMzguMzc1IDEyMi44NjNDMTM4LjE1MyAxMjMuNTUxIDEzNy44NTYgMTI0LjE0NSAxMzcuNDgzIDEyNC42NDZDMTM3LjExMSAxMjUuMTQ4IDEzNi42NTYgMTI1LjUzNSAxMzYuMTE5IDEyNS44MDdDMTM1LjU4OSAxMjYuMDc5IDEzNC45NyAxMjYuMjE1IDEzNC4yNjEgMTI2LjIxNUMxMzMuNTU5IDEyNi4yMTUgMTMyLjkyNSAxMjYuMDY4IDEzMi4zNTkgMTI1Ljc3NEMxMzEuODAxIDEyNS40ODEgMTMxLjMyNSAxMjUuMDY5IDEzMC45MzEgMTI0LjUzOUMxMzAuNTM3IDEyNC4wMDkgMTMwLjIzNiAxMjMuMzg2IDEzMC4wMjggMTIyLjY3QzEyOS44MjEgMTIxLjk0NyAxMjkuNzE3IDEyMS4xNjIgMTI5LjcxNyAxMjAuMzE3Wk0xMzIuMzA2IDEyMC4wOTJWMTIwLjMxN0MxMzIuMzA2IDEyMC44NDcgMTMyLjM1MiAxMjEuMzQxIDEzMi40NDUgMTIxLjhDMTMyLjU0NiAxMjIuMjU4IDEzMi43IDEyMi42NjMgMTMyLjkwNyAxMjMuMDE0QzEzMy4xMTUgMTIzLjM1NyAxMzMuMzgzIDEyMy42MyAxMzMuNzEzIDEyMy44M0MxMzQuMDQ5IDEyNC4wMjMgMTM0LjQ1MSAxMjQuMTIgMTM0LjkxNiAxMjQuMTJDMTM1LjUwMyAxMjQuMTIgMTM1Ljk4NyAxMjMuOTkxIDEzNi4zNjYgMTIzLjczM0MxMzYuNzQ2IDEyMy40NzYgMTM3LjA0MyAxMjMuMTI4IDEzNy4yNTggMTIyLjY5MUMxMzcuNDggMTIyLjI0NyAxMzcuNjMgMTIxLjc1MyAxMzcuNzA5IDEyMS4yMDlWMTE5LjI2NUMxMzcuNjY2IDExOC44NDIgMTM3LjU3NiAxMTguNDQ4IDEzNy40NCAxMTguMDgzQzEzNy4zMTIgMTE3LjcxOCAxMzcuMTM2IDExNy4zOTkgMTM2LjkxNCAxMTcuMTI3QzEzNi42OTIgMTE2Ljg0OCAxMzYuNDE2IDExNi42MzMgMTM2LjA4NyAxMTYuNDgyQzEzNS43NjUgMTE2LjMyNSAxMzUuMzgyIDExNi4yNDYgMTM0LjkzOCAxMTYuMjQ2QzEzNC40NjUgMTE2LjI0NiAxMzQuMDY0IDExNi4zNDYgMTMzLjczNCAxMTYuNTQ3QzEzMy40MDUgMTE2Ljc0NyAxMzMuMTMzIDExNy4wMjMgMTMyLjkxOCAxMTcuMzc0QzEzMi43MSAxMTcuNzI1IDEzMi41NTYgMTE4LjEzMyAxMzIuNDU2IDExOC41OTlDMTMyLjM1NiAxMTkuMDY0IDEzMi4zMDYgMTE5LjU2MiAxMzIuMzA2IDEyMC4wOTJaIiBmaWxsPSIjM0Y1MkREIi8+Cjwvc3ZnPgo=", - "description": null, + "description": "Facilitates user interaction by enabling navigation between dashboard states, sending RPC commands to devices, and updating device attributes or time-series data.", "order": 7500, "name": "Buttons" }, "widgetTypeFqns": [ "action_button", - "command_button" + "command_button", + "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 5dce630e64..560063a2c4 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -9,6 +9,9 @@ }, "widgetTypeFqns": [ "single_switch", + "command_button", + "power_button", + "slider", "control_widgets.switch_control", "control_widgets.slide_toggle_control", "control_widgets.round_switch", diff --git a/application/src/main/data/json/system/widget_types/action_button.json b/application/src/main/data/json/system/widget_types/action_button.json index e6ffa8084e..a73aa198e7 100644 --- a/application/src/main/data/json/system/widget_types/action_button.json +++ b/application/src/main/data/json/system/widget_types/action_button.json @@ -3,7 +3,7 @@ "name": "Action button", "deprecated": false, "image": "tb-image:YWN0aW9uLWJ1dHRvbi5zdmc=:IkFjdGlvbiBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzUiIHk9IjUwLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjAuNzUiIHk9IjUwLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIHN0cm9rZT0iIzNGNTJERCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPHBhdGggZD0iTTYyLjE2NzMgODkuMzMzM1Y4Mi4zMzMzSDY2LjgzNFY4OS4zMzMzSDcyLjY2NzNWODBINzYuMTY3M0w2NC41MDA3IDY5LjVMNTIuODM0IDgwSDU2LjMzNFY4OS4zMzMzSDYyLjE2NzNaIiBmaWxsPSIjM0Y1MkREIi8+CjxwYXRoIGQ9Ik05MC4xOTUzIDgwLjkzMTZIODYuMjFMODYuMTg4NSA3OC45NjU4SDg5LjY2ODlDOTAuMjU2MiA3OC45NjU4IDkwLjc1MzkgNzguODc5OSA5MS4xNjIxIDc4LjcwOEM5MS41Nzc1IDc4LjUyOSA5MS44OTI2IDc4LjI3NDcgOTIuMTA3NCA3Ny45NDUzQzkyLjMyMjMgNzcuNjA4NyA5Mi40Mjk3IDc3LjIwNDEgOTIuNDI5NyA3Ni43MzE0QzkyLjQyOTcgNzYuMjA4NyA5Mi4zMjk0IDc1Ljc4MjYgOTIuMTI4OSA3NS40NTMxQzkxLjkyODQgNzUuMTIzNyA5MS42MjA0IDc0Ljg4MzggOTEuMjA1MSA3NC43MzM0QzkwLjc5NjkgNzQuNTgzIDkwLjI3NDEgNzQuNTA3OCA4OS42MzY3IDc0LjUwNzhIODcuMDI2NFY4OEg4NC4zMzAxVjcyLjM1OTRIODkuNjM2N0M5MC40OTYxIDcyLjM1OTQgOTEuMjYyNCA3Mi40NDE3IDkxLjkzNTUgNzIuNjA2NEM5Mi42MTU5IDcyLjc3MTIgOTMuMTkyNCA3My4wMjkgOTMuNjY1IDczLjM3OTlDOTQuMTQ0OSA3My43MjM2IDk0LjUwNjUgNzQuMTYwNSA5NC43NSA3NC42OTA0Qzk1LjAwMDcgNzUuMjIwNCA5NS4xMjYgNzUuODUwNiA5NS4xMjYgNzYuNTgxMUM5NS4xMjYgNzcuMjI1NiA5NC45NzIgNzcuODE2NCA5NC42NjQxIDc4LjM1MzVDOTQuMzU2MSA3OC44ODM1IDkzLjkwMTQgNzkuMzE2NyA5My4yOTk4IDc5LjY1MzNDOTIuNjk4MiA3OS45ODk5IDkxLjk0OTkgODAuMTkwNCA5MS4wNTQ3IDgwLjI1NDlMOTAuMTk1MyA4MC45MzE2Wk05MC4wNzcxIDg4SDg1LjM2MTNMODYuNTc1MiA4NS44NjIzSDkwLjA3NzFDOTAuNjg1OSA4NS44NjIzIDkxLjE5NDMgODUuNzYyIDkxLjYwMjUgODUuNTYxNUM5Mi4wMTA3IDg1LjM1MzggOTIuMzE1MSA4NS4wNzEgOTIuNTE1NiA4NC43MTI5QzkyLjcyMzMgODQuMzQ3NyA5Mi44MjcxIDgzLjkyMTUgOTIuODI3MSA4My40MzQ2QzkyLjgyNzEgODIuOTI2MSA5Mi43Mzc2IDgyLjQ4NTcgOTIuNTU4NiA4Mi4xMTMzQzkyLjM3OTYgODEuNzMzNyA5Mi4wOTY3IDgxLjQ0MzcgOTEuNzEgODEuMjQzMkM5MS4zMjMyIDgxLjAzNTUgOTAuODE4NCA4MC45MzE2IDkwLjE5NTMgODAuOTMxNkg4Ny4xNjZMODcuMTg3NSA3OC45NjU4SDkxLjEyOTlMOTEuNzQyMiA3OS43MDdDOTIuNjAxNiA3OS43MzU3IDkzLjMwNyA3OS45MjU1IDkzLjg1ODQgODAuMjc2NEM5NC40MTcgODAuNjI3MyA5NC44MzI0IDgxLjA4MiA5NS4xMDQ1IDgxLjY0MDZDOTUuMzc2NiA4Mi4xOTkyIDk1LjUxMjcgODIuODAwOCA5NS41MTI3IDgzLjQ0NTNDOTUuNTEyNyA4NC40NDA4IDk1LjI5NDMgODUuMjc1MSA5NC44NTc0IDg1Ljk0ODJDOTQuNDI3NyA4Ni42MjE0IDkzLjgwODMgODcuMTMzNSA5Mi45OTkgODcuNDg0NEM5Mi4xODk4IDg3LjgyODEgOTEuMjE1OCA4OCA5MC4wNzcxIDg4Wk0xMDUuMjE2IDg1LjI2MDdWNzYuMzc3SDEwNy44MTVWODhIMTA1LjM2NkwxMDUuMjE2IDg1LjI2MDdaTTEwNS41ODEgODIuODQzOEwxMDYuNDUxIDgyLjgyMjNDMTA2LjQ1MSA4My42MDI5IDEwNi4zNjUgODQuMzIyNiAxMDYuMTkzIDg0Ljk4MTRDMTA2LjAyMSA4NS42MzMxIDEwNS43NTcgODYuMjAyNSAxMDUuMzk4IDg2LjY4OTVDMTA1LjA0IDg3LjE2OTMgMTA0LjU4MiA4Ny41NDUyIDEwNC4wMjMgODcuODE3NEMxMDMuNDY1IDg4LjA4MjQgMTAyLjc5NSA4OC4yMTQ4IDEwMi4wMTUgODguMjE0OEMxMDEuNDQ5IDg4LjIxNDggMTAwLjkzIDg4LjEzMjUgMTAwLjQ1NyA4Ny45Njc4Qzk5Ljk4NDQgODcuODAzMSA5OS41NzYyIDg3LjU0ODggOTkuMjMyNCA4Ny4yMDUxQzk4Ljg5NTggODYuODYxMyA5OC42MzQ0IDg2LjQxMzcgOTguNDQ4MiA4NS44NjIzQzk4LjI2MiA4NS4zMTA5IDk4LjE2ODkgODQuNjUyIDk4LjE2ODkgODMuODg1N1Y3Ni4zNzdIMTAwLjc1OFY4My45MDcyQzEwMC43NTggODQuMzI5OCAxMDAuODA4IDg0LjY4NDIgMTAwLjkwOCA4NC45NzA3QzEwMS4wMDggODUuMjUgMTAxLjE0NSA4NS40NzU2IDEwMS4zMTYgODUuNjQ3NUMxMDEuNDg4IDg1LjgxOTMgMTAxLjY4OSA4NS45NDExIDEwMS45MTggODYuMDEyN0MxMDIuMTQ3IDg2LjA4NDMgMTAyLjM5MSA4Ni4xMjAxIDEwMi42NDggODYuMTIwMUMxMDMuMzg2IDg2LjEyMDEgMTAzLjk2NiA4NS45NzY5IDEwNC4zODkgODUuNjkwNEMxMDQuODE4IDg1LjM5NjggMTA1LjEyMyA4NS4wMDI5IDEwNS4zMDIgODQuNTA4OEMxMDUuNDg4IDg0LjAxNDYgMTA1LjU4MSA4My40NTk2IDEwNS41ODEgODIuODQzOFpNMTE2LjA0NyA3Ni4zNzdWNzguMjY3NkgxMDkuNDk0Vjc2LjM3N0gxMTYuMDQ3Wk0xMTEuMzg1IDczLjUzMDNIMTEzLjk3NFY4NC43ODgxQzExMy45NzQgODUuMTQ2MiAxMTQuMDI0IDg1LjQyMTkgMTE0LjEyNCA4NS42MTUyQzExNC4yMzEgODUuODAxNCAxMTQuMzc4IDg1LjkyNjggMTE0LjU2NCA4NS45OTEyQzExNC43NTEgODYuMDU1NyAxMTQuOTY5IDg2LjA4NzkgMTE1LjIyIDg2LjA4NzlDMTE1LjM5OSA4Ni4wODc5IDExNS41NzEgODYuMDc3MSAxMTUuNzM1IDg2LjA1NTdDMTE1LjkgODYuMDM0MiAxMTYuMDMzIDg2LjAxMjcgMTE2LjEzMyA4NS45OTEyTDExNi4xNDQgODcuOTY3OEMxMTUuOTI5IDg4LjAzMjIgMTE1LjY3OCA4OC4wODk1IDExNS4zOTIgODguMTM5NkMxMTUuMTEyIDg4LjE4OTggMTE0Ljc5IDg4LjIxNDggMTE0LjQyNSA4OC4yMTQ4QzExMy44MyA4OC4yMTQ4IDExMy4zMDQgODguMTExIDExMi44NDYgODcuOTAzM0MxMTIuMzg3IDg3LjY4ODUgMTEyLjAyOSA4Ny4zNDExIDExMS43NzEgODYuODYxM0MxMTEuNTE0IDg2LjM4MTUgMTExLjM4NSA4NS43NDQxIDExMS4zODUgODQuOTQ5MlY3My41MzAzWk0xMjMuNjIzIDc2LjM3N1Y3OC4yNjc2SDExNy4wN1Y3Ni4zNzdIMTIzLjYyM1pNMTE4Ljk2MSA3My41MzAzSDEyMS41NVY4NC43ODgxQzEyMS41NSA4NS4xNDYyIDEyMS42IDg1LjQyMTkgMTIxLjcgODUuNjE1MkMxMjEuODA4IDg1LjgwMTQgMTIxLjk1NCA4NS45MjY4IDEyMi4xNDEgODUuOTkxMkMxMjIuMzI3IDg2LjA1NTcgMTIyLjU0NSA4Ni4wODc5IDEyMi43OTYgODYuMDg3OUMxMjIuOTc1IDg2LjA4NzkgMTIzLjE0NyA4Ni4wNzcxIDEyMy4zMTIgODYuMDU1N0MxMjMuNDc2IDg2LjAzNDIgMTIzLjYwOSA4Ni4wMTI3IDEyMy43MDkgODUuOTkxMkwxMjMuNzIgODcuOTY3OEMxMjMuNTA1IDg4LjAzMjIgMTIzLjI1NCA4OC4wODk1IDEyMi45NjggODguMTM5NkMxMjIuNjg4IDg4LjE4OTggMTIyLjM2NiA4OC4yMTQ4IDEyMi4wMDEgODguMjE0OEMxMjEuNDA3IDg4LjIxNDggMTIwLjg4IDg4LjExMSAxMjAuNDIyIDg3LjkwMzNDMTE5Ljk2NCA4Ny42ODg1IDExOS42MDUgODcuMzQxMSAxMTkuMzQ4IDg2Ljg2MTNDMTE5LjA5IDg2LjM4MTUgMTE4Ljk2MSA4NS43NDQxIDExOC45NjEgODQuOTQ5MlY3My41MzAzWk0xMjUuMTE5IDgyLjMxNzRWODIuMDcwM0MxMjUuMTE5IDgxLjIzMjQgMTI1LjI0MSA4MC40NTU0IDEyNS40ODQgNzkuNzM5M0MxMjUuNzI4IDc5LjAxNiAxMjYuMDc5IDc4LjM4OTMgMTI2LjUzNyA3Ny44NTk0QzEyNy4wMDMgNzcuMzIyMyAxMjcuNTY4IDc2LjkwNjkgMTI4LjIzNCA3Ni42MTMzQzEyOC45MDggNzYuMzEyNSAxMjkuNjY3IDc2LjE2MjEgMTMwLjUxMiA3Ni4xNjIxQzEzMS4zNjQgNzYuMTYyMSAxMzIuMTIzIDc2LjMxMjUgMTMyLjc4OSA3Ni42MTMzQzEzMy40NjIgNzYuOTA2OSAxMzQuMDMyIDc3LjMyMjMgMTM0LjQ5NyA3Ny44NTk0QzEzNC45NjMgNzguMzg5MyAxMzUuMzE3IDc5LjAxNiAxMzUuNTYxIDc5LjczOTNDMTM1LjgwNCA4MC40NTU0IDEzNS45MjYgODEuMjMyNCAxMzUuOTI2IDgyLjA3MDNWODIuMzE3NEMxMzUuOTI2IDgzLjE1NTMgMTM1LjgwNCA4My45MzIzIDEzNS41NjEgODQuNjQ4NEMxMzUuMzE3IDg1LjM2NDYgMTM0Ljk2MyA4NS45OTEyIDEzNC40OTcgODYuNTI4M0MxMzQuMDMyIDg3LjA1ODMgMTMzLjQ2NiA4Ny40NzM2IDEzMi44IDg3Ljc3NDRDMTMyLjEzNCA4OC4wNjggMTMxLjM3OCA4OC4yMTQ4IDEzMC41MzMgODguMjE0OEMxMjkuNjgxIDg4LjIxNDggMTI4LjkxOCA4OC4wNjggMTI4LjI0NSA4Ny43NzQ0QzEyNy41NzkgODcuNDczNiAxMjcuMDEzIDg3LjA1ODMgMTI2LjU0OCA4Ni41MjgzQzEyNi4wODIgODUuOTkxMiAxMjUuNzI4IDg1LjM2NDYgMTI1LjQ4NCA4NC42NDg0QzEyNS4yNDEgODMuOTMyMyAxMjUuMTE5IDgzLjE1NTMgMTI1LjExOSA4Mi4zMTc0Wk0xMjcuNzA4IDgyLjA3MDNWODIuMzE3NEMxMjcuNzA4IDgyLjg0MDIgMTI3Ljc2MiA4My4zMzQzIDEyNy44NjkgODMuNzk5OEMxMjcuOTc3IDg0LjI2NTMgMTI4LjE0NSA4NC42NzM1IDEyOC4zNzQgODUuMDI0NEMxMjguNjAzIDg1LjM3NTMgMTI4Ljg5NyA4NS42NTEgMTI5LjI1NSA4NS44NTE2QzEyOS42MTMgODYuMDUyMSAxMzAuMDM5IDg2LjE1MjMgMTMwLjUzMyA4Ni4xNTIzQzEzMS4wMTMgODYuMTUyMyAxMzEuNDI4IDg2LjA1MjEgMTMxLjc3OSA4NS44NTE2QzEzMi4xMzcgODUuNjUxIDEzMi40MzEgODUuMzc1MyAxMzIuNjYgODUuMDI0NEMxMzIuODg5IDg0LjY3MzUgMTMzLjA1OCA4NC4yNjUzIDEzMy4xNjUgODMuNzk5OEMxMzMuMjggODMuMzM0MyAxMzMuMzM3IDgyLjg0MDIgMTMzLjMzNyA4Mi4zMTc0VjgyLjA3MDNDMTMzLjMzNyA4MS41NTQ3IDEzMy4yOCA4MS4wNjc3IDEzMy4xNjUgODAuNjA5NEMxMzMuMDU4IDgwLjE0MzkgMTMyLjg4NiA3OS43MzIxIDEzMi42NDkgNzkuMzc0QzEzMi40MiA3OS4wMTYgMTMyLjEyNyA3OC43MzY3IDEzMS43NjkgNzguNTM2MUMxMzEuNDE4IDc4LjMyODUgMTMwLjk5OSA3OC4yMjQ2IDEzMC41MTIgNzguMjI0NkMxMzAuMDI1IDc4LjIyNDYgMTI5LjYwMiA3OC4zMjg1IDEyOS4yNDQgNzguNTM2MUMxMjguODkzIDc4LjczNjcgMTI4LjYwMyA3OS4wMTYgMTI4LjM3NCA3OS4zNzRDMTI4LjE0NSA3OS43MzIxIDEyNy45NzcgODAuMTQzOSAxMjcuODY5IDgwLjYwOTRDMTI3Ljc2MiA4MS4wNjc3IDEyNy43MDggODEuNTU0NyAxMjcuNzA4IDgyLjA3MDNaTTE0MC45MTMgNzguODU4NFY4OEgxMzguMzI0Vjc2LjM3N0gxNDAuNzYzTDE0MC45MTMgNzguODU4NFpNMTQwLjQ1MSA4MS43NTg4TDEzOS42MTMgODEuNzQ4QzEzOS42MiA4MC45MjQ1IDEzOS43MzUgODAuMTY4OSAxMzkuOTU3IDc5LjQ4MTRDMTQwLjE4NiA3OC43OTM5IDE0MC41MDEgNzguMjAzMSAxNDAuOTAyIDc3LjcwOUMxNDEuMzExIDc3LjIxNDggMTQxLjc5OCA3Ni44MzUzIDE0Mi4zNjMgNzYuNTcwM0MxNDIuOTI5IDc2LjI5ODIgMTQzLjU1OSA3Ni4xNjIxIDE0NC4yNTQgNzYuMTYyMUMxNDQuODEyIDc2LjE2MjEgMTQ1LjMxNyA3Ni4yNDA5IDE0NS43NjkgNzYuMzk4NEMxNDYuMjI3IDc2LjU0ODggMTQ2LjYxNyA3Ni43OTU5IDE0Ni45MzkgNzcuMTM5NkMxNDcuMjY5IDc3LjQ4MzQgMTQ3LjUyIDc3LjkzMSAxNDcuNjkxIDc4LjQ4MjRDMTQ3Ljg2MyA3OS4wMjY3IDE0Ny45NDkgNzkuNjk2MyAxNDcuOTQ5IDgwLjQ5MTJWODhIMTQ1LjM1VjgwLjQ4MDVDMTQ1LjM1IDc5LjkyMTkgMTQ1LjI2NyA3OS40ODE0IDE0NS4xMDMgNzkuMTU5MkMxNDQuOTQ1IDc4LjgyOTggMTQ0LjcxMiA3OC41OTcgMTQ0LjQwNCA3OC40NjA5QzE0NC4xMDQgNzguMzE3NyAxNDMuNzI4IDc4LjI0NjEgMTQzLjI3NiA3OC4yNDYxQzE0Mi44MzIgNzguMjQ2MSAxNDIuNDM1IDc4LjMzOTIgMTQyLjA4NCA3OC41MjU0QzE0MS43MzMgNzguNzExNiAxNDEuNDM2IDc4Ljk2NTggMTQxLjE5MiA3OS4yODgxQzE0MC45NTYgNzkuNjEwNCAxNDAuNzczIDc5Ljk4MjcgMTQwLjY0NSA4MC40MDUzQzE0MC41MTYgODAuODI3OCAxNDAuNDUxIDgxLjI3OSAxNDAuNDUxIDgxLjc1ODhaIiBmaWxsPSIjM0Y1MkREIi8+Cjwvc3ZnPgo=", - "description": "Action button.", + "description": "Facilitates single-click navigation to other dashboards, states, or custom actions. Configurable settings allow for on-click action definition and conditions for button activation or deactivation. It offers various layouts and custom styling options for different states.", "descriptor": { "type": "latest", "sizeX": 3, @@ -22,6 +22,8 @@ "tags": [ "button", "action", - "navigation" + "navigation", + "navigate", + "dashboard state" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/command_button.json b/application/src/main/data/json/system/widget_types/command_button.json index 1ffd2326a8..dcfeb108f2 100644 --- a/application/src/main/data/json/system/widget_types/command_button.json +++ b/application/src/main/data/json/system/widget_types/command_button.json @@ -3,7 +3,7 @@ "name": "Command button", "deprecated": false, "image": "tb-image:Y29tbWFuZC1idXR0b24uc3Zn:IkNvbW1hbmQgYnV0dG9uIiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNzUiIHk9IjUwLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjAuNzUiIHk9IjUwLjc1IiB3aWR0aD0iMTk4LjUiIGhlaWdodD0iNTguNSIgcng9IjMuMjUiIHN0cm9rZT0iIzNGNTJERCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPHBhdGggZD0iTTY1LjQ5OTcgNzNWNzUuMzMzM0g3NS41MjEzTDY0LjMzMyA4Ni41MjE3TDY1Ljk3OCA4OC4xNjY3TDc3LjE2NjMgNzYuOTc4M1Y4N0g3OS40OTk3VjczSDY1LjQ5OTdaIiBmaWxsPSIjM0Y1MkREIi8+CjxwYXRoIGQ9Ik0xMDAuNTY0IDgzLjk3MTdDMTAwLjU2NCA4My42NDk0IDEwMC41MTQgODMuMzYzIDEwMC40MTQgODMuMTEyM0MxMDAuMzIxIDgyLjg2MTcgMTAwLjE1MyA4Mi42MzI1IDk5LjkwOTIgODIuNDI0OEM5OS42NjU3IDgyLjIxNzEgOTkuMzIxOSA4Mi4wMTY2IDk4Ljg3NzkgODEuODIzMkM5OC40NDExIDgxLjYyMjcgOTcuODgyNSA4MS40MTg2IDk3LjIwMjEgODEuMjEwOUM5Ni40NTc0IDgwLjk4MTggOTUuNzY5OSA4MC43Mjc1IDk1LjEzOTYgODAuNDQ4MkM5NC41MTY2IDgwLjE2MTggOTMuOTcyMyA3OS44MzI0IDkzLjUwNjggNzkuNDZDOTMuMDQxMyA3OS4wODA0IDkyLjY3OTcgNzguNjQ3MSA5Mi40MjE5IDc4LjE2MDJDOTIuMTY0MSA3Ny42NjYgOTIuMDM1MiA3Ny4wOTY3IDkyLjAzNTIgNzYuNDUyMUM5Mi4wMzUyIDc1LjgxNDggOTIuMTY3NiA3NS4yMzQ3IDkyLjQzMjYgNzQuNzExOUM5Mi43MDQ4IDc0LjE4OTEgOTMuMDg3OSA3My43MzggOTMuNTgyIDczLjM1ODRDOTQuMDgzMyA3Mi45NzE3IDk0LjY3NDIgNzIuNjc0NSA5NS4zNTQ1IDcyLjQ2NjhDOTYuMDM0OCA3Mi4yNTIgOTYuNzg2OCA3Mi4xNDQ1IDk3LjYxMDQgNzIuMTQ0NUM5OC43NzA1IDcyLjE0NDUgOTkuNzY5NSA3Mi4zNTk0IDEwMC42MDcgNzIuNzg5MUMxMDEuNDUyIDczLjIxODggMTAyLjEwMSA3My43OTUyIDEwMi41NTIgNzQuNTE4NkMxMDMuMDEgNzUuMjQxOSAxMDMuMjM5IDc2LjA0MDQgMTAzLjIzOSA3Ni45MTQxSDEwMC41NjRDMTAwLjU2NCA3Ni4zOTg0IDEwMC40NTMgNzUuOTQzNyAxMDAuMjMxIDc1LjU0OThDMTAwLjAxNyA3NS4xNDg4IDk5LjY4NzIgNzQuODMzNyA5OS4yNDMyIDc0LjYwNDVDOTguODA2MyA3NC4zNzUzIDk4LjI1MTMgNzQuMjYwNyA5Ny41NzgxIDc0LjI2MDdDOTYuOTQwOCA3NC4yNjA3IDk2LjQxMDggNzQuMzU3NCA5NS45ODgzIDc0LjU1MDhDOTUuNTY1OCA3NC43NDQxIDk1LjI1MDcgNzUuMDA1NSA5NS4wNDMgNzUuMzM1Qzk0LjgzNTMgNzUuNjY0NCA5NC43MzE0IDc2LjAzNjggOTQuNzMxNCA3Ni40NTIxQzk0LjczMTQgNzYuNzQ1OCA5NC43OTk1IDc3LjAxNDMgOTQuOTM1NSA3Ny4yNTc4Qzk1LjA3MTYgNzcuNDk0MSA5NS4yNzkzIDc3LjcxNjEgOTUuNTU4NiA3Ny45MjM4Qzk1LjgzNzkgNzguMTI0MyA5Ni4xODg4IDc4LjMxNDEgOTYuNjExMyA3OC40OTMyQzk3LjAzMzkgNzguNjcyMiA5Ny41MzE2IDc4Ljg0NDEgOTguMTA0NSA3OS4wMDg4Qzk4Ljk3MSA3OS4yNjY2IDk5LjcyNjYgNzkuNTUzMSAxMDAuMzcxIDc5Ljg2ODJDMTAxLjAxNiA4MC4xNzYxIDEwMS41NTMgODAuNTI3IDEwMS45ODIgODAuOTIwOUMxMDIuNDEyIDgxLjMxNDggMTAyLjczNCA4MS43NjI0IDEwMi45NDkgODIuMjYzN0MxMDMuMTY0IDgyLjc1NzggMTAzLjI3MSA4My4zMiAxMDMuMjcxIDgzLjk1MDJDMTAzLjI3MSA4NC42MDkgMTAzLjEzOSA4NS4yMDM1IDEwMi44NzQgODUuNzMzNEMxMDIuNjA5IDg2LjI1NjIgMTAyLjIyOSA4Ni43MDM4IDEwMS43MzUgODcuMDc2MkMxMDEuMjQ4IDg3LjQ0MTQgMTAwLjY2MSA4Ny43MjQzIDk5Ljk3MzYgODcuOTI0OEM5OS4yOTMzIDg4LjExODIgOTguNTM0MiA4OC4yMTQ4IDk3LjY5NjMgODguMjE0OEM5Ni45NDQzIDg4LjIxNDggOTYuMjAzMSA4OC4xMTQ2IDk1LjQ3MjcgODcuOTE0MUM5NC43NDkzIDg3LjcxMzUgOTQuMDkwNSA4Ny40MDkyIDkzLjQ5NjEgODcuMDAxQzkyLjkwMTcgODYuNTg1NiA5Mi40MjkgODYuMDcgOTIuMDc4MSA4NS40NTQxQzkxLjcyNzIgODQuODMxMSA5MS41NTE4IDg0LjEwNDIgOTEuNTUxOCA4My4yNzM0SDk0LjI0OEM5NC4yNDggODMuNzgxOSA5NC4zMzQgODQuMjE1MiA5NC41MDU5IDg0LjU3MzJDOTQuNjg0OSA4NC45MzEzIDk0LjkzMiA4NS4yMjQ5IDk1LjI0NzEgODUuNDU0MUM5NS41NjIyIDg1LjY3NjEgOTUuOTI3NCA4NS44NDA4IDk2LjM0MjggODUuOTQ4MkM5Ni43NjUzIDg2LjA1NTcgOTcuMjE2NSA4Ni4xMDk0IDk3LjY5NjMgODYuMTA5NEM5OC4zMjY1IDg2LjEwOTQgOTguODUyOSA4Ni4wMTk5IDk5LjI3NTQgODUuODQwOEM5OS43MDUxIDg1LjY2MTggMTAwLjAyNyA4NS40MTExIDEwMC4yNDIgODUuMDg4OUMxMDAuNDU3IDg0Ljc2NjYgMTAwLjU2NCA4NC4zOTQyIDEwMC41NjQgODMuOTcxN1pNMTEwLjc3MiA4OC4yMTQ4QzEwOS45MTMgODguMjE0OCAxMDkuMTM2IDg4LjA3NTIgMTA4LjQ0MSA4Ny43OTU5QzEwNy43NTQgODcuNTA5NCAxMDcuMTY3IDg3LjExMiAxMDYuNjggODYuNjAzNUMxMDYuMiA4Ni4wOTUxIDEwNS44MzEgODUuNDk3MSAxMDUuNTczIDg0LjgwOTZDMTA1LjMxNSA4NC4xMjIxIDEwNS4xODcgODMuMzgwOSAxMDUuMTg3IDgyLjU4NTlWODIuMTU2MkMxMDUuMTg3IDgxLjI0NjcgMTA1LjMxOSA4MC40MjMyIDEwNS41ODQgNzkuNjg1NUMxMDUuODQ5IDc4Ljk0NzkgMTA2LjIxOCA3OC4zMTc3IDEwNi42OSA3Ny43OTQ5QzEwNy4xNjMgNzcuMjY1IDEwNy43MjIgNzYuODYwNCAxMDguMzY2IDc2LjU4MTFDMTA5LjAxMSA3Ni4zMDE4IDEwOS43MDkgNzYuMTYyMSAxMTAuNDYxIDc2LjE2MjFDMTExLjI5MiA3Ni4xNjIxIDExMi4wMTkgNzYuMzAxOCAxMTIuNjQyIDc2LjU4MTFDMTEzLjI2NSA3Ni44NjA0IDExMy43OCA3Ny4yNTQyIDExNC4xODggNzcuNzYyN0MxMTQuNjA0IDc4LjI2NCAxMTQuOTEyIDc4Ljg2MiAxMTUuMTEyIDc5LjU1NjZDMTE1LjMyIDgwLjI1MTMgMTE1LjQyNCA4MS4wMTc2IDExNS40MjQgODEuODU1NVY4Mi45NjE5SDEwNi40NDNWODEuMTAzNUgxMTIuODY3VjgwLjg5OTRDMTEyLjg1MyA4MC40MzM5IDExMi43NiA3OS45OTcxIDExMi41ODggNzkuNTg4OUMxMTIuNDIzIDc5LjE4MDcgMTEyLjE2OSA3OC44NTEyIDExMS44MjUgNzguNjAwNkMxMTEuNDgxIDc4LjM0OTkgMTExLjAyMyA3OC4yMjQ2IDExMC40NSA3OC4yMjQ2QzExMC4wMjEgNzguMjI0NiAxMDkuNjM3IDc4LjMxNzcgMTA5LjMwMSA3OC41MDM5QzEwOC45NzEgNzguNjgyOSAxMDguNjk2IDc4Ljk0NDMgMTA4LjQ3NCA3OS4yODgxQzEwOC4yNTIgNzkuNjMxOCAxMDguMDggODAuMDQ3MiAxMDcuOTU4IDgwLjUzNDJDMTA3Ljg0MyA4MS4wMTQgMTA3Ljc4NiA4MS41NTQ3IDEwNy43ODYgODIuMTU2MlY4Mi41ODU5QzEwNy43ODYgODMuMDk0NCAxMDcuODU0IDgzLjU2NzEgMTA3Ljk5IDg0LjAwMzlDMTA4LjEzMyA4NC40MzM2IDEwOC4zNDEgODQuODA5NiAxMDguNjEzIDg1LjEzMThDMTA4Ljg4NSA4NS40NTQxIDEwOS4yMTUgODUuNzA4MyAxMDkuNjAyIDg1Ljg5NDVDMTA5Ljk4OCA4Ni4wNzM2IDExMC40MjkgODYuMTYzMSAxMTAuOTIzIDg2LjE2MzFDMTExLjU0NiA4Ni4xNjMxIDExMi4xMDEgODYuMDM3OCAxMTIuNTg4IDg1Ljc4NzFDMTEzLjA3NSA4NS41MzY1IDExMy40OTcgODUuMTgyIDExMy44NTUgODQuNzIzNkwxMTUuMjIgODYuMDQ0OUMxMTQuOTY5IDg2LjQxMDIgMTE0LjY0MyA4Ni43NjExIDExNC4yNDIgODcuMDk3N0MxMTMuODQxIDg3LjQyNzEgMTEzLjM1MSA4Ny42OTU2IDExMi43NzEgODcuOTAzM0MxMTIuMTk4IDg4LjExMSAxMTEuNTMyIDg4LjIxNDggMTEwLjc3MiA4OC4yMTQ4Wk0xMjAuMjYxIDc4Ljg1ODRWODhIMTE3LjY3MlY3Ni4zNzdIMTIwLjExTDEyMC4yNjEgNzguODU4NFpNMTE5Ljc5OSA4MS43NTg4TDExOC45NjEgODEuNzQ4QzExOC45NjggODAuOTI0NSAxMTkuMDgzIDgwLjE2ODkgMTE5LjMwNSA3OS40ODE0QzExOS41MzQgNzguNzkzOSAxMTkuODQ5IDc4LjIwMzEgMTIwLjI1IDc3LjcwOUMxMjAuNjU4IDc3LjIxNDggMTIxLjE0NSA3Ni44MzUzIDEyMS43MTEgNzYuNTcwM0MxMjIuMjc3IDc2LjI5ODIgMTIyLjkwNyA3Ni4xNjIxIDEyMy42MDIgNzYuMTYyMUMxMjQuMTYgNzYuMTYyMSAxMjQuNjY1IDc2LjI0MDkgMTI1LjExNiA3Ni4zOTg0QzEyNS41NzUgNzYuNTQ4OCAxMjUuOTY1IDc2Ljc5NTkgMTI2LjI4NyA3Ny4xMzk2QzEyNi42MTcgNzcuNDgzNCAxMjYuODY3IDc3LjkzMSAxMjcuMDM5IDc4LjQ4MjRDMTI3LjIxMSA3OS4wMjY3IDEyNy4yOTcgNzkuNjk2MyAxMjcuMjk3IDgwLjQ5MTJWODhIMTI0LjY5N1Y4MC40ODA1QzEyNC42OTcgNzkuOTIxOSAxMjQuNjE1IDc5LjQ4MTQgMTI0LjQ1IDc5LjE1OTJDMTI0LjI5MyA3OC44Mjk4IDEyNC4wNiA3OC41OTcgMTIzLjc1MiA3OC40NjA5QzEyMy40NTEgNzguMzE3NyAxMjMuMDc1IDc4LjI0NjEgMTIyLjYyNCA3OC4yNDYxQzEyMi4xOCA3OC4yNDYxIDEyMS43ODMgNzguMzM5MiAxMjEuNDMyIDc4LjUyNTRDMTIxLjA4MSA3OC43MTE2IDEyMC43ODQgNzguOTY1OCAxMjAuNTQgNzkuMjg4MUMxMjAuMzA0IDc5LjYxMDQgMTIwLjEyMSA3OS45ODI3IDExOS45OTIgODAuNDA1M0MxMTkuODYzIDgwLjgyNzggMTE5Ljc5OSA4MS4yNzkgMTE5Ljc5OSA4MS43NTg4Wk0xMzcuMjc5IDg1LjU5MzhWNzEuNUgxMzkuODc5Vjg4SDEzNy41MjZMMTM3LjI3OSA4NS41OTM4Wk0xMjkuNzE3IDgyLjMxNzRWODIuMDkxOEMxMjkuNzE3IDgxLjIxMDkgMTI5LjgyMSA4MC40MDg5IDEzMC4wMjggNzkuNjg1NUMxMzAuMjM2IDc4Ljk1NTEgMTMwLjUzNyA3OC4zMjg1IDEzMC45MzEgNzcuODA1N0MxMzEuMzI1IDc3LjI3NTcgMTMxLjgwNCA3Ni44NzExIDEzMi4zNyA3Ni41OTE4QzEzMi45MzYgNzYuMzA1MyAxMzMuNTczIDc2LjE2MjEgMTM0LjI4MiA3Ni4xNjIxQzEzNC45ODQgNzYuMTYyMSAxMzUuNiA3Ni4yOTgyIDEzNi4xMyA3Ni41NzAzQzEzNi42NiA3Ni44NDI0IDEzNy4xMTEgNzcuMjMyNyAxMzcuNDgzIDc3Ljc0MTJDMTM3Ljg1NiA3OC4yNDI1IDEzOC4xNTMgNzguODQ0MSAxMzguMzc1IDc5LjU0NTlDMTM4LjU5NyA4MC4yNDA2IDEzOC43NTUgODEuMDE0IDEzOC44NDggODEuODY2MlY4Mi41ODU5QzEzOC43NTUgODMuNDE2NyAxMzguNTk3IDg0LjE3NTggMTM4LjM3NSA4NC44NjMzQzEzOC4xNTMgODUuNTUwOCAxMzcuODU2IDg2LjE0NTIgMTM3LjQ4MyA4Ni42NDY1QzEzNy4xMTEgODcuMTQ3OCAxMzYuNjU2IDg3LjUzNDUgMTM2LjExOSA4Ny44MDY2QzEzNS41ODkgODguMDc4OCAxMzQuOTcgODguMjE0OCAxMzQuMjYxIDg4LjIxNDhDMTMzLjU1OSA4OC4yMTQ4IDEzMi45MjUgODguMDY4IDEzMi4zNTkgODcuNzc0NEMxMzEuODAxIDg3LjQ4MDggMTMxLjMyNSA4Ny4wNjkgMTMwLjkzMSA4Ni41MzkxQzEzMC41MzcgODYuMDA5MSAxMzAuMjM2IDg1LjM4NjEgMTMwLjAyOCA4NC42Njk5QzEyOS44MjEgODMuOTQ2NiAxMjkuNzE3IDgzLjE2MjQgMTI5LjcxNyA4Mi4zMTc0Wk0xMzIuMzA2IDgyLjA5MThWODIuMzE3NEMxMzIuMzA2IDgyLjg0NzMgMTMyLjM1MiA4My4zNDE1IDEzMi40NDUgODMuNzk5OEMxMzIuNTQ2IDg0LjI1ODEgMTMyLjcgODQuNjYyOCAxMzIuOTA3IDg1LjAxMzdDMTMzLjExNSA4NS4zNTc0IDEzMy4zODMgODUuNjI5NiAxMzMuNzEzIDg1LjgzMDFDMTM0LjA0OSA4Ni4wMjM0IDEzNC40NTEgODYuMTIwMSAxMzQuOTE2IDg2LjEyMDFDMTM1LjUwMyA4Ni4xMjAxIDEzNS45ODcgODUuOTkxMiAxMzYuMzY2IDg1LjczMzRDMTM2Ljc0NiA4NS40NzU2IDEzNy4wNDMgODUuMTI4MyAxMzcuMjU4IDg0LjY5MTRDMTM3LjQ4IDg0LjI0NzQgMTM3LjYzIDgzLjc1MzMgMTM3LjcwOSA4My4yMDlWODEuMjY0NkMxMzcuNjY2IDgwLjg0MjEgMTM3LjU3NiA4MC40NDgyIDEzNy40NCA4MC4wODNDMTM3LjMxMiA3OS43MTc4IDEzNy4xMzYgNzkuMzk5MSAxMzYuOTE0IDc5LjEyN0MxMzYuNjkyIDc4Ljg0NzcgMTM2LjQxNiA3OC42MzI4IDEzNi4wODcgNzguNDgyNEMxMzUuNzY1IDc4LjMyNDkgMTM1LjM4MiA3OC4yNDYxIDEzNC45MzggNzguMjQ2MUMxMzQuNDY1IDc4LjI0NjEgMTM0LjA2NCA3OC4zNDY0IDEzMy43MzQgNzguNTQ2OUMxMzMuNDA1IDc4Ljc0NzQgMTMzLjEzMyA3OS4wMjMxIDEzMi45MTggNzkuMzc0QzEzMi43MSA3OS43MjQ5IDEzMi41NTYgODAuMTMzMSAxMzIuNDU2IDgwLjU5ODZDMTMyLjM1NiA4MS4wNjQxIDEzMi4zMDYgODEuNTYxOCAxMzIuMzA2IDgyLjA5MThaIiBmaWxsPSIjM0Y1MkREIi8+Cjwvc3ZnPgo=", - "description": "Sends the command to the device or updates attribute/time-series when the user clicks the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when click.", + "description": "Allows single-click commands to devices or updates to attributes/time-series. Settings enable definition of the on-click action and condition when the button is disabled. Supports multiple layouts and custom styles for different states.", "descriptor": { "type": "rpc", "sizeX": 3, @@ -31,6 +31,9 @@ "subroutine call", "inter-process communication", "server request", - "button" + "button", + "update attribute", + "set attribute", + "add time-series" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/power_button.json b/application/src/main/data/json/system/widget_types/power_button.json new file mode 100644 index 0000000000..d90439d82f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/power_button.json @@ -0,0 +1,36 @@ +{ + "fqn": "power_button", + "name": "Power button", + "deprecated": false, + "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=", + "description": "Sends the command to the device or updates attribute/time-series when the user pushes the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when power on/off states.", + "descriptor": { + "type": "rpc", + "sizeX": 3.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-power-button-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-power-button-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Power\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":null}" + }, + "tags": [ + "command", + "downlink", + "device configuration", + "device control", + "invocation", + "remote method", + "remote function", + "interface", + "subroutine call", + "inter-process communication", + "server request", + "power" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/progress_bar.json b/application/src/main/data/json/system/widget_types/progress_bar.json index 1a92b6d470..6e60c20942 100644 --- a/application/src/main/data/json/system/widget_types/progress_bar.json +++ b/application/src/main/data/json/system/widget_types/progress_bar.json @@ -2,7 +2,7 @@ "fqn": "progress_bar", "name": "Progress bar", "deprecated": false, - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAeFBMVEXg4ODf39/g4ODg4OAAAAD////g4OD19fUhISE/Ut3j4+OsrKzHx8eQkJB0dHQ8PDzx8fEvLy+6urrV1dWCgoJYWFienp5KSkrv7++GhoZmZmaXl5f39/epqanLy8vCwsL29vZtfORiceJ+fn7m5ubU1NTn6fve4fIbcz38AAAABXRSTlPvIL+vAC9A4IoAAASZSURBVHja7d0NT9swEIDhMji4nb/jxEk/NmCM7f//w/niQLtJyxamtdfiF6EmDkh+1Di0IojVzdWH1e25d311s7paDXD2+cy49nABDfm0gotodSmQ2woRVoVIq0KkVSHSqhBpVYi0KkRaFSKtGUijctrDv6XQwEzHgDjkXLoAiKaoAurzhxAAGGxBm2gMgE+25SHeUNE0QKbRNo/otlWQo2QNQa6xrdpD1DSqW6vyBhmtrD86RKHNGIcOyOUHZJDDEEIe1xgQdT487gGF0CNmgUXnsH+BhPzhCKDFfNh50OgQ6dgQv0HFEB2hw8QYDQo3HlqGZFf0066GDiNQFmvcMEZNEMdH2zza8WhivopwVEjX9QF7yBCVSbwFCc04YdBoy4xhkw3QYAsWjQciiNhH8OT3a8RnXo4ooRm/M3dMSN91m+QnCM+6zN9h2ShTyq4xy08X9mpcVxgswR4yfktymDsJhICbIPEA4n+GJJXTfNS48atIWQzxABKQl0YEJQDiQ/Dl1LeoDyHtONWYj41TDtDYvJMwTZA2w7ADwwNJAAQS9sqgY4NLyb1CKGCrGNeh0QY7Pq6bHvUEQaMcKh5VJkiAgAmIHfGGw7B5hQD1iMHkR4vlC5TLA+l1jeS9dlo5LdojQeYj/7rBp8xrnvz+kSOCg349WjolpES9itphBEm9BeI3OJ46omLI8qhppP2e6z28sTqvKkRaFSKtCpFWhUirQqRVIdKqEGlViLQqRFoVIq0KkVaFSKtCpFUh0qoQaVWItCpEWhUirQqR1jzER609HEbSbhT4G4g3AXOWYKrpAnYgszlIjyVXJNTl7XOEJMQ+ArXT5L3LpBQJZDYD6RGJAQED5DKoBbnN3uTfvIAAgBB7EFyBzBbQlXsTNV/EQGh/hPgWUZW7e1PglR+hJOw6PA/RfCs2Gsg5nApFYvtO1L2A8xC1n3ne6DWQRewKEfQGSM7FeB5CSrUOUY0QN13D0DPEQuzANiAlhsxHAYNnSA9ci6gZ5Jq2VRbEVCCzWcRYrl2cKRAgk6gT9NPx9xBvbTqYeofBTyz9IlTaiDm3Zp6RckoxAePIScxzGKBEPTnVK5DRDMS+vtZyLMguBbTZv1BxpAw0UpbJDMQ7LAWCXIMl52EsGVAtKCmvv2YgQNYxY0PTbs97xh9SN07KemfITFHHw4nzntA3jO/kPfsZVSHSqhBpVYi0KkRaFSKtCpHWO4B8e/y4rC/f7/5/D8shXz4u7fHuCO0WQz4u7+4YLYY8LnZ8uTtCu+VrZOm59XiMNbJ7x1etM6tCpFUh0qoQac1A7s325L+P2t1/AoBhu90O04SWQ4b1ANsTP10Pa7XOD5/u4fMWbte74WlYDnlOAM3Jn5KBIesBHp7gXmXS5+WQz5/EQOz42TQVIgWyHgDyqfVGyPD0AOkZThxD+JrznBf7NpO+LodAszZbOHEF8nW9ZYEyZvnll9vtQErDNKH3/ZP9vKoQaVWItCpEWhUirQqRVoVIq0KkVSHSqhBpVYi0LgiygovodvXhAv71NIC/Xl2txPzpxNsbMuPm6vr23Ft9uLr5AdpbpSAPQyy7AAAAAElFTkSuQmCC", + "image": "tb-image:cHJvZ3Jlc3NfYmFyX3N5c3RlbV93aWRnZXRfaW1hZ2UucG5n:IlByb2dyZXNzIGJhciIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF80Njg1XzQwOTg5KSI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjBfZF80Njg1XzQwOTg5KSI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNCIgZmlsbD0id2hpdGUiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMV9iXzQ2ODVfNDA5ODkpIj4KPHJlY3QgeD0iMTIiIHk9IjEyIiB3aWR0aD0iMTc2IiBoZWlnaHQ9IjEzNiIgcng9IjQiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9nPgo8cGF0aCBkPSJNMTYuNDQ5NyAxOS41NTMySDE0LjA0MzlWMTguMjlIMTYuNDQ5N0MxNi44Njg3IDE4LjI5IDE3LjIwNzIgMTguMjIyMyAxNy40NjUzIDE4LjA4NjlDMTcuNzIzNSAxNy45NTE1IDE3LjkxMTggMTcuNzY1MyAxOC4wMzAzIDE3LjUyODNDMTguMTUzIDE3LjI4NzEgMTguMjE0NCAxNy4wMTIgMTguMjE0NCAxNi43MDMxQzE4LjIxNDQgMTYuNDExMSAxOC4xNTMgMTYuMTM4MiAxOC4wMzAzIDE1Ljg4NDNDMTcuOTExOCAxNS42MjYxIDE3LjcyMzUgMTUuNDE4OCAxNy40NjUzIDE1LjI2MjJDMTcuMjA3MiAxNS4xMDU2IDE2Ljg2ODcgMTUuMDI3MyAxNi40NDk3IDE1LjAyNzNIMTQuNTMyN1YyM0gxMi45Mzk1VjEzLjc1NzhIMTYuNDQ5N0MxNy4xNjQ5IDEzLjc1NzggMTcuNzcyMSAxMy44ODQ4IDE4LjI3MTUgMTQuMTM4N0MxOC43NzUxIDE0LjM4ODMgMTkuMTU4IDE0LjczNTQgMTkuNDIwNCAxNS4xNzk3QzE5LjY4MjggMTUuNjE5OCAxOS44MTQgMTYuMTIzNCAxOS44MTQgMTYuNjkwNEMxOS44MTQgMTcuMjg3MSAxOS42ODI4IDE3Ljc5OTIgMTkuNDIwNCAxOC4yMjY2QzE5LjE1OCAxOC42NTQgMTguNzc1MSAxOC45ODE5IDE4LjI3MTUgMTkuMjEwNEMxNy43NzIxIDE5LjQzOSAxNy4xNjQ5IDE5LjU1MzIgMTYuNDQ5NyAxOS41NTMyWk0yMi44NzYgMTcuNDM5NVYyM0gyMS4zNDYyVjE2LjEzMThIMjIuODA2MkwyMi44NzYgMTcuNDM5NVpNMjQuOTc3MSAxNi4wODc0TDI0Ljk2NDQgMTcuNTA5M0MyNC44NzEzIDE3LjQ5MjQgMjQuNzY5NyAxNy40Nzk3IDI0LjY1OTcgMTcuNDcxMkMyNC41NTM5IDE3LjQ2MjcgMjQuNDQ4MSAxNy40NTg1IDI0LjM0MjMgMTcuNDU4NUMyNC4wNzk5IDE3LjQ1ODUgMjMuODQ5MyAxNy40OTY2IDIzLjY1MDQgMTcuNTcyOEMyMy40NTE1IDE3LjY0NDcgMjMuMjg0MyAxNy43NTA1IDIzLjE0ODkgMTcuODkwMUMyMy4wMTc3IDE4LjAyNTYgMjIuOTE2MiAxOC4xOTA2IDIyLjg0NDIgMTguMzg1M0MyMi43NzIzIDE4LjU3OTkgMjIuNzMgMTguNzk3OSAyMi43MTczIDE5LjAzOTFMMjIuMzY4MiAxOS4wNjQ1QzIyLjM2ODIgMTguNjMyOCAyMi40MTA1IDE4LjIzMjkgMjIuNDk1MSAxNy44NjQ3QzIyLjU3OTggMTcuNDk2NiAyMi43MDY3IDE3LjE3MjkgMjIuODc2IDE2Ljg5MzZDMjMuMDQ5NSAxNi42MTQzIDIzLjI2NTMgMTYuMzk2MyAyMy41MjM0IDE2LjIzOTdDMjMuNzg1OCAxNi4wODMyIDI0LjA4ODQgMTYuMDA0OSAyNC40MzEyIDE2LjAwNDlDMjQuNTI0MyAxNi4wMDQ5IDI0LjYyMzcgMTYuMDEzMyAyNC43Mjk1IDE2LjAzMDNDMjQuODM5NSAxNi4wNDcyIDI0LjkyMiAxNi4wNjYyIDI0Ljk3NzEgMTYuMDg3NFpNMjUuNzI4NSAxOS42NDIxVjE5LjQ5NjFDMjUuNzI4NSAxOS4wMDEgMjUuODAwNSAxOC41NDE4IDI1Ljk0NDMgMTguMTE4N0MyNi4wODgyIDE3LjY5MTIgMjYuMjk1NiAxNy4zMjEgMjYuNTY2NCAxNy4wMDc4QzI2Ljg0MTUgMTYuNjkwNCAyNy4xNzU4IDE2LjQ0NSAyNy41NjkzIDE2LjI3MTVDMjcuOTY3MSAxNi4wOTM4IDI4LjQxNTcgMTYuMDA0OSAyOC45MTUgMTYuMDA0OUMyOS40MTg2IDE2LjAwNDkgMjkuODY3MiAxNi4wOTM4IDMwLjI2MDcgMTYuMjcxNUMzMC42NTg1IDE2LjQ0NSAzMC45OTUgMTYuNjkwNCAzMS4yNyAxNy4wMDc4QzMxLjU0NTEgMTcuMzIxIDMxLjc1NDYgMTcuNjkxMiAzMS44OTg0IDE4LjExODdDMzIuMDQyMyAxOC41NDE4IDMyLjExNDMgMTkuMDAxIDMyLjExNDMgMTkuNDk2MVYxOS42NDIxQzMyLjExNDMgMjAuMTM3MiAzMi4wNDIzIDIwLjU5NjQgMzEuODk4NCAyMS4wMTk1QzMxLjc1NDYgMjEuNDQyNyAzMS41NDUxIDIxLjgxMyAzMS4yNyAyMi4xMzA0QzMwLjk5NSAyMi40NDM1IDMwLjY2MDYgMjIuNjg5IDMwLjI2NzEgMjIuODY2N0MyOS44NzM1IDIzLjA0MDIgMjkuNDI3MSAyMy4xMjcgMjguOTI3NyAyMy4xMjdDMjguNDI0MiAyMy4xMjcgMjcuOTczNSAyMy4wNDAyIDI3LjU3NTcgMjIuODY2N0MyNy4xODIxIDIyLjY4OSAyNi44NDc4IDIyLjQ0MzUgMjYuNTcyOCAyMi4xMzA0QzI2LjI5NzcgMjEuODEzIDI2LjA4ODIgMjEuNDQyNyAyNS45NDQzIDIxLjAxOTVDMjUuODAwNSAyMC41OTY0IDI1LjcyODUgMjAuMTM3MiAyNS43Mjg1IDE5LjY0MjFaTTI3LjI1ODMgMTkuNDk2MVYxOS42NDIxQzI3LjI1ODMgMTkuOTUxIDI3LjI5IDIwLjI0MyAyNy4zNTM1IDIwLjUxODFDMjcuNDE3IDIwLjc5MzEgMjcuNTE2NCAyMS4wMzQzIDI3LjY1MTkgMjEuMjQxN0MyNy43ODczIDIxLjQ0OTEgMjcuOTYwOCAyMS42MTIgMjguMTcyNCAyMS43MzA1QzI4LjM4NCAyMS44NDkgMjguNjM1NyAyMS45MDgyIDI4LjkyNzcgMjEuOTA4MkMyOS4yMTEzIDIxLjkwODIgMjkuNDU2NyAyMS44NDkgMjkuNjY0MSAyMS43MzA1QzI5Ljg3NTcgMjEuNjEyIDMwLjA0OTIgMjEuNDQ5MSAzMC4xODQ2IDIxLjI0MTdDMzAuMzIgMjEuMDM0MyAzMC40MTk0IDIwLjc5MzEgMzAuNDgyOSAyMC41MTgxQzMwLjU1MDYgMjAuMjQzIDMwLjU4NDUgMTkuOTUxIDMwLjU4NDUgMTkuNjQyMVYxOS40OTYxQzMwLjU4NDUgMTkuMTkxNCAzMC41NTA2IDE4LjkwMzYgMzAuNDgyOSAxOC42MzI4QzMwLjQxOTQgMTguMzU3NyAzMC4zMTc5IDE4LjExNDQgMzAuMTc4MiAxNy45MDI4QzMwLjA0MjggMTcuNjkxMiAyOS44NjkzIDE3LjUyNjIgMjkuNjU3NyAxNy40MDc3QzI5LjQ1MDQgMTcuMjg1IDI5LjIwMjggMTcuMjIzNiAyOC45MTUgMTcuMjIzNkMyOC42MjczIDE3LjIyMzYgMjguMzc3NiAxNy4yODUgMjguMTY2IDE3LjQwNzdDMjcuOTU4NyAxNy41MjYyIDI3Ljc4NzMgMTcuNjkxMiAyNy42NTE5IDE3LjkwMjhDMjcuNTE2NCAxOC4xMTQ0IDI3LjQxNyAxOC4zNTc3IDI3LjM1MzUgMTguNjMyOEMyNy4yOSAxOC45MDM2IDI3LjI1ODMgMTkuMTkxNCAyNy4yNTgzIDE5LjQ5NjFaTTM4LjA0NTQgMTYuMTMxOEgzOS40MzU1VjIyLjgwOTZDMzkuNDM1NSAyMy40Mjc0IDM5LjMwNDQgMjMuOTUyMSAzOS4wNDIgMjQuMzgzOEMzOC43Nzk2IDI0LjgxNTQgMzguNDEzNiAyNS4xNDM0IDM3Ljk0MzggMjUuMzY3N0MzNy40NzQxIDI1LjU5NjIgMzYuOTMwMyAyNS43MTA0IDM2LjMxMjUgMjUuNzEwNEMzNi4wNTAxIDI1LjcxMDQgMzUuNzU4MSAyNS42NzI0IDM1LjQzNjUgMjUuNTk2MkMzNS4xMTkxIDI1LjUyIDM0LjgxMDIgMjUuMzk3MyAzNC41MDk4IDI1LjIyOEMzNC4yMTM1IDI1LjA2MyAzMy45NjYgMjQuODQ1MSAzMy43NjcxIDI0LjU3NDJMMzQuNDg0NCAyMy42NzI5QzM0LjcyOTggMjMuOTY0OCAzNS4wMDA3IDI0LjE3ODUgMzUuMjk2OSAyNC4zMTRDMzUuNTkzMSAyNC40NDk0IDM1LjkwNDEgMjQuNTE3MSAzNi4yMyAyNC41MTcxQzM2LjU4MTIgMjQuNTE3MSAzNi44Nzk2IDI0LjQ1MTUgMzcuMTI1IDI0LjMyMDNDMzcuMzc0NyAyNC4xOTM0IDM3LjU2NzIgMjQuMDA1IDM3LjcwMjYgMjMuNzU1NEMzNy44MzgxIDIzLjUwNTcgMzcuOTA1OCAyMy4yMDEgMzcuOTA1OCAyMi44NDEzVjE3LjY4N0wzOC4wNDU0IDE2LjEzMThaTTMzLjM3OTkgMTkuNjQyMVYxOS41MDg4QzMzLjM3OTkgMTguOTg4MyAzMy40NDM0IDE4LjUxNDMgMzMuNTcwMyAxOC4wODY5QzMzLjY5NzMgMTcuNjU1MyAzMy44NzkyIDE3LjI4NSAzNC4xMTYyIDE2Ljk3NjFDMzQuMzUzMiAxNi42NjI5IDM0LjY0MSAxNi40MjM4IDM0Ljk3OTUgMTYuMjU4OEMzNS4zMTggMTYuMDg5NSAzNS43MDEgMTYuMDA0OSAzNi4xMjg0IDE2LjAwNDlDMzYuNTcyOCAxNi4wMDQ5IDM2Ljk1MTUgMTYuMDg1MyAzNy4yNjQ2IDE2LjI0NjFDMzcuNTgyIDE2LjQwNjkgMzcuODQ2NSAxNi42Mzc1IDM4LjA1ODEgMTYuOTM4QzM4LjI2OTcgMTcuMjM0MiAzOC40MzQ3IDE3LjU4OTcgMzguNTUzMiAxOC4wMDQ0QzM4LjY3NTkgMTguNDE0OSAzOC43NjY5IDE4Ljg3MTkgMzguODI2MiAxOS4zNzU1VjE5LjgwMDhDMzguNzcxMiAyMC4yOTE3IDM4LjY3ODEgMjAuNzQwMiAzOC41NDY5IDIxLjE0NjVDMzguNDE1NyAyMS41NTI3IDM4LjI0MjIgMjEuOTA0IDM4LjAyNjQgMjIuMjAwMkMzNy44MTA1IDIyLjQ5NjQgMzcuNTQzOSAyMi43MjQ5IDM3LjIyNjYgMjIuODg1N0MzNi45MTM0IDIzLjA0NjUgMzYuNTQzMSAyMy4xMjcgMzYuMTE1NyAyMy4xMjdDMzUuNjk2OCAyMy4xMjcgMzUuMzE4IDIzLjA0MDIgMzQuOTc5NSAyMi44NjY3QzM0LjY0NTIgMjIuNjkzMiAzNC4zNTc0IDIyLjQ0OTkgMzQuMTE2MiAyMi4xMzY3QzMzLjg3OTIgMjEuODIzNiAzMy42OTczIDIxLjQ1NTQgMzMuNTcwMyAyMS4wMzIyQzMzLjQ0MzQgMjAuNjA0OCAzMy4zNzk5IDIwLjE0MTQgMzMuMzc5OSAxOS42NDIxWk0zNC45MDk3IDE5LjUwODhWMTkuNjQyMUMzNC45MDk3IDE5Ljk1NTIgMzQuOTM5MyAyMC4yNDcyIDM0Ljk5ODUgMjAuNTE4MUMzNS4wNjIgMjAuNzg4OSAzNS4xNTcyIDIxLjAyOCAzNS4yODQyIDIxLjIzNTRDMzUuNDE1NCAyMS40Mzg1IDM1LjU4MDQgMjEuNTk5MyAzNS43NzkzIDIxLjcxNzhDMzUuOTgyNCAyMS44MzIgMzYuMjIxNSAyMS44ODkyIDM2LjQ5NjYgMjEuODg5MkMzNi44NTYzIDIxLjg4OTIgMzcuMTUwNCAyMS44MTMgMzcuMzc4OSAyMS42NjA2QzM3LjYxMTcgMjEuNTA4MyAzNy43ODk0IDIxLjMwMzEgMzcuOTEyMSAyMS4wNDQ5QzM4LjAzOTEgMjAuNzgyNiAzOC4xMjc5IDIwLjQ5MDYgMzguMTc4NyAyMC4xNjg5VjE5LjAyQzM4LjE1MzMgMTguNzcwMyAzOC4xMDA0IDE4LjUzNzYgMzguMDIgMTguMzIxOEMzNy45NDM4IDE4LjEwNiAzNy44NDAyIDE3LjkxNzYgMzcuNzA5IDE3Ljc1NjhDMzcuNTc3OCAxNy41OTE4IDM3LjQxMjggMTcuNDY0OCAzNy4yMTM5IDE3LjM3NkMzNy4wMTUgMTcuMjgyOSAzNi43ODAxIDE3LjIzNjMgMzYuNTA5MyAxNy4yMzYzQzM2LjIzNDIgMTcuMjM2MyAzNS45OTUxIDE3LjI5NTYgMzUuNzkyIDE3LjQxNDFDMzUuNTg4OSAxNy41MzI2IDM1LjQyMTcgMTcuNjk1NSAzNS4yOTA1IDE3LjkwMjhDMzUuMTYzNiAxOC4xMTAyIDM1LjA2ODQgMTguMzUxNCAzNS4wMDQ5IDE4LjYyNjVDMzQuOTQxNCAxOC45MDE1IDM0LjkwOTcgMTkuMTk1NiAzNC45MDk3IDE5LjUwODhaTTQyLjgwODYgMTcuNDM5NVYyM0g0MS4yNzg4VjE2LjEzMThINDIuNzM4OEw0Mi44MDg2IDE3LjQzOTVaTTQ0LjkwOTcgMTYuMDg3NEw0NC44OTcgMTcuNTA5M0M0NC44MDM5IDE3LjQ5MjQgNDQuNzAyMyAxNy40Nzk3IDQ0LjU5MjMgMTcuNDcxMkM0NC40ODY1IDE3LjQ2MjcgNDQuMzgwNyAxNy40NTg1IDQ0LjI3NDkgMTcuNDU4NUM0NC4wMTI1IDE3LjQ1ODUgNDMuNzgxOSAxNy40OTY2IDQzLjU4MyAxNy41NzI4QzQzLjM4NDEgMTcuNjQ0NyA0My4yMTcgMTcuNzUwNSA0My4wODE1IDE3Ljg5MDFDNDIuOTUwNCAxOC4wMjU2IDQyLjg0ODggMTguMTkwNiA0Mi43NzY5IDE4LjM4NTNDNDIuNzA0OSAxOC41Nzk5IDQyLjY2MjYgMTguNzk3OSA0Mi42NDk5IDE5LjAzOTFMNDIuMzAwOCAxOS4wNjQ1QzQyLjMwMDggMTguNjMyOCA0Mi4zNDMxIDE4LjIzMjkgNDIuNDI3NyAxNy44NjQ3QzQyLjUxMjQgMTcuNDk2NiA0Mi42MzkzIDE3LjE3MjkgNDIuODA4NiAxNi44OTM2QzQyLjk4MjEgMTYuNjE0MyA0My4xOTc5IDE2LjM5NjMgNDMuNDU2MSAxNi4yMzk3QzQzLjcxODQgMTYuMDgzMiA0NC4wMjEgMTYuMDA0OSA0NC4zNjM4IDE2LjAwNDlDNDQuNDU2OSAxNi4wMDQ5IDQ0LjU1NjMgMTYuMDEzMyA0NC42NjIxIDE2LjAzMDNDNDQuNzcyMSAxNi4wNDcyIDQ0Ljg1NDcgMTYuMDY2MiA0NC45MDk3IDE2LjA4NzRaTTQ5LjAxOSAyMy4xMjdDNDguNTExMiAyMy4xMjcgNDguMDUyMSAyMy4wNDQ0IDQ3LjY0MTYgMjIuODc5NEM0Ny4yMzU0IDIyLjcxMDEgNDYuODg4MyAyMi40NzUzIDQ2LjYwMDYgMjIuMTc0OEM0Ni4zMTcxIDIxLjg3NDMgNDYuMDk5MSAyMS41MjEgNDUuOTQ2OCAyMS4xMTQ3QzQ1Ljc5NDQgMjAuNzA4NSA0NS43MTgzIDIwLjI3MDUgNDUuNzE4MyAxOS44MDA4VjE5LjU0NjlDNDUuNzE4MyAxOS4wMDk0IDQ1Ljc5NjUgMTguNTIyOCA0NS45NTMxIDE4LjA4NjlDNDYuMTA5NyAxNy42NTEgNDYuMzI3NiAxNy4yNzg2IDQ2LjYwNjkgMTYuOTY5N0M0Ni44ODYyIDE2LjY1NjYgNDcuMjE2MyAxNi40MTc1IDQ3LjU5NzIgMTYuMjUyNEM0Ny45NzggMTYuMDg3NCA0OC4zOTA2IDE2LjAwNDkgNDguODM1IDE2LjAwNDlDNDkuMzI1OCAxNi4wMDQ5IDQ5Ljc1NTQgMTYuMDg3NCA1MC4xMjM1IDE2LjI1MjRDNTAuNDkxNyAxNi40MTc1IDUwLjc5NjQgMTYuNjUwMiA1MS4wMzc2IDE2Ljk1MDdDNTEuMjgzIDE3LjI0NjkgNTEuNDY1IDE3LjYwMDMgNTEuNTgzNSAxOC4wMTA3QzUxLjcwNjIgMTguNDIxMiA1MS43Njc2IDE4Ljg3NCA1MS43Njc2IDE5LjM2OTFWMjAuMDIyOUg0Ni40NjA5VjE4LjkyNDhINTAuMjU2OFYxOC44MDQyQzUwLjI0ODQgMTguNTI5MSA1MC4xOTM0IDE4LjI3MSA1MC4wOTE4IDE4LjAyOThDNDkuOTk0NSAxNy43ODg2IDQ5Ljg0NDIgMTcuNTkzOSA0OS42NDExIDE3LjQ0NThDNDkuNDM4IDE3LjI5NzcgNDkuMTY3MiAxNy4yMjM2IDQ4LjgyODYgMTcuMjIzNkM0OC41NzQ3IDE3LjIyMzYgNDguMzQ4MyAxNy4yNzg2IDQ4LjE0OTQgMTcuMzg4N0M0Ny45NTQ4IDE3LjQ5NDUgNDcuNzkxOCAxNy42NDg5IDQ3LjY2MDYgMTcuODUyMUM0Ny41Mjk1IDE4LjA1NTIgNDcuNDI3OSAxOC4zMDA2IDQ3LjM1NiAxOC41ODg0QzQ3LjI4ODIgMTguODcxOSA0Ny4yNTQ0IDE5LjE5MTQgNDcuMjU0NCAxOS41NDY5VjE5LjgwMDhDNDcuMjU0NCAyMC4xMDEyIDQ3LjI5NDYgMjAuMzgwNSA0Ny4zNzUgMjAuNjM4N0M0Ny40NTk2IDIwLjg5MjYgNDcuNTgyNCAyMS4xMTQ3IDQ3Ljc0MzIgMjEuMzA1MkM0Ny45MDQgMjEuNDk1NiA0OC4wOTg2IDIxLjY0NTggNDguMzI3MSAyMS43NTU5QzQ4LjU1NTcgMjEuODYxNyA0OC44MTU5IDIxLjkxNDYgNDkuMTA3OSAyMS45MTQ2QzQ5LjQ3NjEgMjEuOTE0NiA0OS44MDQgMjEuODQwNSA1MC4wOTE4IDIxLjY5MjRDNTAuMzc5NiAyMS41NDQzIDUwLjYyOTIgMjEuMzM0OCA1MC44NDA4IDIxLjA2NEw1MS42NDcgMjEuODQ0N0M1MS40OTg5IDIyLjA2MDUgNTEuMzA2MyAyMi4yNjc5IDUxLjA2OTMgMjIuNDY2OEM1MC44MzI0IDIyLjY2MTUgNTAuNTQyNSAyMi44MjAxIDUwLjE5OTcgMjIuOTQyOUM0OS44NjEyIDIzLjA2NTYgNDkuNDY3NiAyMy4xMjcgNDkuMDE5IDIzLjEyN1pNNTcuMDY0IDIxLjE0MDFDNTcuMDY0IDIwLjk4NzggNTcuMDI1OSAyMC44NTAzIDU2Ljk0OTcgMjAuNzI3NUM1Ni44NzM1IDIwLjYwMDYgNTYuNzI3NSAyMC40ODYzIDU2LjUxMTcgMjAuMzg0OEM1Ni4zMDAxIDIwLjI4MzIgNTUuOTg3IDIwLjE5MDEgNTUuNTcyMyAyMC4xMDU1QzU1LjIwODMgMjAuMDI1MSA1NC44NzQgMTkuOTI5OSA1NC41NjkzIDE5LjgxOThDNTQuMjY4OSAxOS43MDU2IDU0LjAxMDcgMTkuNTY4IDUzLjc5NDkgMTkuNDA3MkM1My41NzkxIDE5LjI0NjQgNTMuNDExOSAxOS4wNTYgNTMuMjkzNSAxOC44MzU5QzUzLjE3NSAxOC42MTU5IDUzLjExNTcgMTguMzYyIDUzLjExNTcgMTguMDc0MkM1My4xMTU3IDE3Ljc5NDkgNTMuMTc3MSAxNy41MzA0IDUzLjI5OTggMTcuMjgwOEM1My40MjI1IDE3LjAzMTEgNTMuNTk4MSAxNi44MTEgNTMuODI2NyAxNi42MjA2QzU0LjA1NTIgMTYuNDMwMiA1NC4zMzI0IDE2LjI3OTkgNTQuNjU4MiAxNi4xNjk5QzU0Ljk4ODMgMTYuMDU5OSA1NS4zNTY0IDE2LjAwNDkgNTUuNzYyNyAxNi4wMDQ5QzU2LjMzODIgMTYuMDA0OSA1Ni44MzEyIDE2LjEwMjIgNTcuMjQxNyAxNi4yOTY5QzU3LjY1NjQgMTYuNDg3MyA1Ny45NzM4IDE2Ljc0NzYgNTguMTkzOCAxNy4wNzc2QzU4LjQxMzkgMTcuNDAzNSA1OC41MjM5IDE3Ljc3MTYgNTguNTIzOSAxOC4xODIxSDU2Ljk5NDFDNTYuOTk0MSAxOC4wMDAyIDU2Ljk0NzYgMTcuODMwOSA1Ni44NTQ1IDE3LjY3NDNDNTYuNzY1NiAxNy41MTM1IDU2LjYzMDIgMTcuMzg0NCA1Ni40NDgyIDE3LjI4NzFDNTYuMjY2MyAxNy4xODU1IDU2LjAzNzggMTcuMTM0OCA1NS43NjI3IDE3LjEzNDhDNTUuNTAwMyAxNy4xMzQ4IDU1LjI4MjQgMTcuMTc3MSA1NS4xMDg5IDE3LjI2MTdDNTQuOTM5NiAxNy4zNDIxIDU0LjgxMjcgMTcuNDQ3OSA1NC43MjggMTcuNTc5MUM1NC42NDc2IDE3LjcxMDMgNTQuNjA3NCAxNy44NTQyIDU0LjYwNzQgMTguMDEwN0M1NC42MDc0IDE4LjEyNSA1NC42Mjg2IDE4LjIyODcgNTQuNjcwOSAxOC4zMjE4QzU0LjcxNzQgMTguNDEwNiA1NC43OTM2IDE4LjQ5MzIgNTQuODk5NCAxOC41NjkzQzU1LjAwNTIgMTguNjQxMyA1NS4xNDkxIDE4LjcwOSA1NS4zMzExIDE4Ljc3MjVDNTUuNTE3MyAxOC44MzU5IDU1Ljc1IDE4Ljg5NzMgNTYuMDI5MyAxOC45NTY1QzU2LjU1NCAxOS4wNjY2IDU3LjAwNDcgMTkuMjA4MyA1Ny4zODEzIDE5LjM4MThDNTcuNzYyMiAxOS41NTExIDU4LjA1NDIgMTkuNzcxMiA1OC4yNTczIDIwLjA0MkM1OC40NjA0IDIwLjMwODYgNTguNTYyIDIwLjY0NzEgNTguNTYyIDIxLjA1NzZDNTguNTYyIDIxLjM2MjMgNTguNDk2NCAyMS42NDE2IDU4LjM2NTIgMjEuODk1NUM1OC4yMzgzIDIyLjE0NTIgNTguMDUyMSAyMi4zNjMxIDU3LjgwNjYgMjIuNTQ5M0M1Ny41NjEyIDIyLjczMTMgNTcuMjY3MSAyMi44NzMgNTYuOTI0MyAyMi45NzQ2QzU2LjU4NTggMjMuMDc2MiA1Ni4yMDQ5IDIzLjEyNyA1NS43ODE3IDIzLjEyN0M1NS4xNTk3IDIzLjEyNyA1NC42MzI4IDIzLjAxNjkgNTQuMjAxMiAyMi43OTY5QzUzLjc2OTUgMjIuNTcyNiA1My40NDE2IDIyLjI4NjkgNTMuMjE3MyAyMS45Mzk5QzUyLjk5NzIgMjEuNTg4NyA1Mi44ODcyIDIxLjIyNDggNTIuODg3MiAyMC44NDgxSDU0LjM2NjJDNTQuMzgzMSAyMS4xMzE3IDU0LjQ2MTQgMjEuMzU4MSA1NC42MDExIDIxLjUyNzNDNTQuNzQ1IDIxLjY5MjQgNTQuOTIyNyAyMS44MTMgNTUuMTM0MyAyMS44ODkyQzU1LjM1MDEgMjEuOTYxMSA1NS41NzIzIDIxLjk5NzEgNTUuODAwOCAyMS45OTcxQzU2LjA3NTggMjEuOTk3MSA1Ni4zMDY1IDIxLjk2MTEgNTYuNDkyNyAyMS44ODkyQzU2LjY3ODkgMjEuODEzIDU2LjgyMDYgMjEuNzExNCA1Ni45MTggMjEuNTg0NUM1Ny4wMTUzIDIxLjQ1MzMgNTcuMDY0IDIxLjMwNTIgNTcuMDY0IDIxLjE0MDFaTTY0LjAwNDQgMjEuMTQwMUM2NC4wMDQ0IDIwLjk4NzggNjMuOTY2MyAyMC44NTAzIDYzLjg5MDEgMjAuNzI3NUM2My44MTQgMjAuNjAwNiA2My42NjggMjAuNDg2MyA2My40NTIxIDIwLjM4NDhDNjMuMjQwNiAyMC4yODMyIDYyLjkyNzQgMjAuMTkwMSA2Mi41MTI3IDIwLjEwNTVDNjIuMTQ4OCAyMC4wMjUxIDYxLjgxNDUgMTkuOTI5OSA2MS41MDk4IDE5LjgxOThDNjEuMjA5MyAxOS43MDU2IDYwLjk1MTIgMTkuNTY4IDYwLjczNTQgMTkuNDA3MkM2MC41MTk1IDE5LjI0NjQgNjAuMzUyNCAxOS4wNTYgNjAuMjMzOSAxOC44MzU5QzYwLjExNTQgMTguNjE1OSA2MC4wNTYyIDE4LjM2MiA2MC4wNTYyIDE4LjA3NDJDNjAuMDU2MiAxNy43OTQ5IDYwLjExNzUgMTcuNTMwNCA2MC4yNDAyIDE3LjI4MDhDNjAuMzYzIDE3LjAzMTEgNjAuNTM4NiAxNi44MTEgNjAuNzY3MSAxNi42MjA2QzYwLjk5NTYgMTYuNDMwMiA2MS4yNzI4IDE2LjI3OTkgNjEuNTk4NiAxNi4xNjk5QzYxLjkyODcgMTYuMDU5OSA2Mi4yOTY5IDE2LjAwNDkgNjIuNzAzMSAxNi4wMDQ5QzYzLjI3ODYgMTYuMDA0OSA2My43NzE2IDE2LjEwMjIgNjQuMTgyMSAxNi4yOTY5QzY0LjU5NjggMTYuNDg3MyA2NC45MTQyIDE2Ljc0NzYgNjUuMTM0MyAxNy4wNzc2QzY1LjM1NDMgMTcuNDAzNSA2NS40NjQ0IDE3Ljc3MTYgNjUuNDY0NCAxOC4xODIxSDYzLjkzNDZDNjMuOTM0NiAxOC4wMDAyIDYzLjg4OCAxNy44MzA5IDYzLjc5NDkgMTcuNjc0M0M2My43MDYxIDE3LjUxMzUgNjMuNTcwNiAxNy4zODQ0IDYzLjM4ODcgMTcuMjg3MUM2My4yMDY3IDE3LjE4NTUgNjIuOTc4MiAxNy4xMzQ4IDYyLjcwMzEgMTcuMTM0OEM2Mi40NDA4IDE3LjEzNDggNjIuMjIyOCAxNy4xNzcxIDYyLjA0OTMgMTcuMjYxN0M2MS44OCAxNy4zNDIxIDYxLjc1MzEgMTcuNDQ3OSA2MS42Njg1IDE3LjU3OTFDNjEuNTg4MSAxNy43MTAzIDYxLjU0NzkgMTcuODU0MiA2MS41NDc5IDE4LjAxMDdDNjEuNTQ3OSAxOC4xMjUgNjEuNTY5IDE4LjIyODcgNjEuNjExMyAxOC4zMjE4QzYxLjY1NzkgMTguNDEwNiA2MS43MzQgMTguNDkzMiA2MS44Mzk4IDE4LjU2OTNDNjEuOTQ1NiAxOC42NDEzIDYyLjA4OTUgMTguNzA5IDYyLjI3MTUgMTguNzcyNUM2Mi40NTc3IDE4LjgzNTkgNjIuNjkwNCAxOC44OTczIDYyLjk2OTcgMTguOTU2NUM2My40OTQ1IDE5LjA2NjYgNjMuOTQ1MSAxOS4yMDgzIDY0LjMyMTggMTkuMzgxOEM2NC43MDI2IDE5LjU1MTEgNjQuOTk0NiAxOS43NzEyIDY1LjE5NzggMjAuMDQyQzY1LjQwMDkgMjAuMzA4NiA2NS41MDI0IDIwLjY0NzEgNjUuNTAyNCAyMS4wNTc2QzY1LjUwMjQgMjEuMzYyMyA2NS40MzY4IDIxLjY0MTYgNjUuMzA1NyAyMS44OTU1QzY1LjE3ODcgMjIuMTQ1MiA2NC45OTI1IDIyLjM2MzEgNjQuNzQ3MSAyMi41NDkzQzY0LjUwMTYgMjIuNzMxMyA2NC4yMDc1IDIyLjg3MyA2My44NjQ3IDIyLjk3NDZDNjMuNTI2MiAyMy4wNzYyIDYzLjE0NTMgMjMuMTI3IDYyLjcyMjIgMjMuMTI3QzYyLjEwMDEgMjMuMTI3IDYxLjU3MzIgMjMuMDE2OSA2MS4xNDE2IDIyLjc5NjlDNjAuNzEgMjIuNTcyNiA2MC4zODIgMjIuMjg2OSA2MC4xNTc3IDIxLjkzOTlDNTkuOTM3NyAyMS41ODg3IDU5LjgyNzYgMjEuMjI0OCA1OS44Mjc2IDIwLjg0ODFINjEuMzA2NkM2MS4zMjM2IDIxLjEzMTcgNjEuNDAxOSAyMS4zNTgxIDYxLjU0MTUgMjEuNTI3M0M2MS42ODU0IDIxLjY5MjQgNjEuODYzMSAyMS44MTMgNjIuMDc0NyAyMS44ODkyQzYyLjI5MDUgMjEuOTYxMSA2Mi41MTI3IDIxLjk5NzEgNjIuNzQxMiAyMS45OTcxQzYzLjAxNjMgMjEuOTk3MSA2My4yNDY5IDIxLjk2MTEgNjMuNDMzMSAyMS44ODkyQzYzLjYxOTMgMjEuODEzIDYzLjc2MTEgMjEuNzExNCA2My44NTg0IDIxLjU4NDVDNjMuOTU1NyAyMS40NTMzIDY0LjAwNDQgMjEuMzA1MiA2NC4wMDQ0IDIxLjE0MDFaTTcwLjU4NTQgMTMuMjVINzIuMTE1MlYyMS41MzM3TDcxLjk2OTIgMjNINzAuNTg1NFYxMy4yNVpNNzYuNTk2NyAxOS41MDI0VjE5LjYzNTdDNzYuNTk2NyAyMC4xNDM2IDc2LjUzOTYgMjAuNjExMiA3Ni40MjUzIDIxLjAzODZDNzYuMzE1MyAyMS40NjE4IDc2LjE0NiAyMS44Mjk5IDc1LjkxNzUgMjIuMTQzMUM3NS42OTMyIDIyLjQ1NjIgNzUuNDEzOSAyMi42OTk1IDc1LjA3OTYgMjIuODczQzc0Ljc0OTUgMjMuMDQyMyA3NC4zNjY1IDIzLjEyNyA3My45MzA3IDIzLjEyN0M3My41MDMzIDIzLjEyNyA3My4xMzA5IDIzLjA0NjUgNzIuODEzNSAyMi44ODU3QzcyLjQ5NjEgMjIuNzI0OSA3Mi4yMjk1IDIyLjQ5NjQgNzIuMDEzNyAyMi4yMDAyQzcxLjgwMjEgMjEuOTA0IDcxLjYzMDcgMjEuNTUwNiA3MS40OTk1IDIxLjE0MDFDNzEuMzY4MyAyMC43Mjk3IDcxLjI3NTIgMjAuMjc2OSA3MS4yMjAyIDE5Ljc4MTdWMTkuMzU2NEM3MS4yNzUyIDE4Ljg1NzEgNzEuMzY4MyAxOC40MDQzIDcxLjQ5OTUgMTcuOTk4QzcxLjYzMDcgMTcuNTg3NiA3MS44MDIxIDE3LjIzNDIgNzIuMDEzNyAxNi45MzhDNzIuMjI5NSAxNi42Mzc1IDcyLjQ5NCAxNi40MDY5IDcyLjgwNzEgMTYuMjQ2MUM3My4xMjQ1IDE2LjA4NTMgNzMuNDk0OCAxNi4wMDQ5IDczLjkxOCAxNi4wMDQ5Qzc0LjM1ODEgMTYuMDA0OSA3NC43NDUzIDE2LjA4OTUgNzUuMDc5NiAxNi4yNTg4Qzc1LjQxODEgMTYuNDI4MSA3NS42OTk1IDE2LjY2OTMgNzUuOTIzOCAxNi45ODI0Qzc2LjE0ODEgMTcuMjkxMyA3Ni4zMTUzIDE3LjY1OTUgNzYuNDI1MyAxOC4wODY5Qzc2LjUzOTYgMTguNTE0MyA3Ni41OTY3IDE4Ljk4NjIgNzYuNTk2NyAxOS41MDI0Wk03NS4wNjY5IDE5LjYzNTdWMTkuNTAyNEM3NS4wNjY5IDE5LjE5MzUgNzUuMDQxNSAxOC45MDM2IDc0Ljk5MDcgMTguNjMyOEM3NC45Mzk5IDE4LjM1NzcgNzQuODU1MyAxOC4xMTY1IDc0LjczNjggMTcuOTA5MkM3NC42MjI2IDE3LjcwMTggNzQuNDY2IDE3LjUzODkgNzQuMjY3MSAxNy40MjA0Qzc0LjA3MjQgMTcuMjk3NyA3My44MjkxIDE3LjIzNjMgNzMuNTM3MSAxNy4yMzYzQzczLjI2NjMgMTcuMjM2MyA3My4wMzM1IDE3LjI4MjkgNzIuODM4OSAxNy4zNzZDNzIuNjQ0MiAxNy40NjkxIDcyLjQ4MTMgMTcuNTk2IDcyLjM1MDEgMTcuNzU2OEM3Mi4yMTg5IDE3LjkxNzYgNzIuMTE1MiAxOC4xMDM4IDcyLjAzOTEgMTguMzE1NEM3MS45NjcxIDE4LjUyNyA3MS45MTg1IDE4Ljc1NTUgNzEuODkzMSAxOS4wMDFWMjAuMTQ5OUM3MS45MzEyIDIwLjQ2NzMgNzIuMDExNiAyMC43NTkzIDcyLjEzNDMgMjEuMDI1OUM3Mi4yNjEyIDIxLjI4ODIgNzIuNDM5IDIxLjQ5OTggNzIuNjY3NSAyMS42NjA2QzcyLjg5NiAyMS44MTcyIDczLjE5MDEgMjEuODk1NSA3My41NDk4IDIxLjg5NTVDNzMuODMzMyAyMS44OTU1IDc0LjA3MjQgMjEuODM4NCA3NC4yNjcxIDIxLjcyNDFDNzQuNDYxOCAyMS42MDk5IDc0LjYxNjIgMjEuNDUxMiA3NC43MzA1IDIxLjI0OEM3NC44NDkgMjEuMDQwNyA3NC45MzM2IDIwLjc5OTUgNzQuOTg0NCAyMC41MjQ0Qzc1LjAzOTQgMjAuMjQ5MyA3NS4wNjY5IDE5Ljk1MzEgNzUuMDY2OSAxOS42MzU3Wk04Mi4wMTM3IDIxLjYyMjZWMTguMzQ3MkM4Mi4wMTM3IDE4LjEwMTcgODEuOTY5MiAxNy44OTAxIDgxLjg4MDQgMTcuNzEyNEM4MS43OTE1IDE3LjUzNDcgODEuNjU2MSAxNy4zOTcxIDgxLjQ3NDEgMTcuMjk5OEM4MS4yOTY0IDE3LjIwMjUgODEuMDcyMSAxNy4xNTM4IDgwLjgwMTMgMTcuMTUzOEM4MC41NTE2IDE3LjE1MzggODAuMzM1OCAxNy4xOTYxIDgwLjE1MzggMTcuMjgwOEM3OS45NzE4IDE3LjM2NTQgNzkuODMwMSAxNy40Nzk3IDc5LjcyODUgMTcuNjIzNUM3OS42MjcgMTcuNzY3NCA3OS41NzYyIDE3LjkzMDMgNzkuNTc2MiAxOC4xMTIzSDc4LjA1MjdDNzguMDUyNyAxNy44NDE1IDc4LjExODMgMTcuNTc5MSA3OC4yNDk1IDE3LjMyNTJDNzguMzgwNyAxNy4wNzEzIDc4LjU3MTEgMTYuODQ0OSA3OC44MjA4IDE2LjY0NkM3OS4wNzA1IDE2LjQ0NzEgNzkuMzY4OCAxNi4yOTA1IDc5LjcxNTggMTYuMTc2M0M4MC4wNjI4IDE2LjA2MiA4MC40NTIxIDE2LjAwNDkgODAuODgzOCAxNi4wMDQ5QzgxLjQwMDEgMTYuMDA0OSA4MS44NTcxIDE2LjA5MTYgODIuMjU0OSAxNi4yNjUxQzgyLjY1NjkgMTYuNDM4NiA4Mi45NzIyIDE2LjcwMSA4My4yMDA3IDE3LjA1MjJDODMuNDMzNCAxNy4zOTkzIDgzLjU0OTggMTcuODM1MSA4My41NDk4IDE4LjM1OTlWMjEuNDEzMUM4My41NDk4IDIxLjcyNjIgODMuNTcxIDIyLjAwNzYgODMuNjEzMyAyMi4yNTczQzgzLjY1OTggMjIuNTAyOCA4My43MjU0IDIyLjcxNjUgODMuODEwMSAyMi44OTg0VjIzSDgyLjI0MjJDODIuMTcwMiAyMi44MzUgODIuMTEzMSAyMi42MjU1IDgyLjA3MDggMjIuMzcxNkM4Mi4wMzI3IDIyLjExMzQgODIuMDEzNyAyMS44NjM4IDgyLjAxMzcgMjEuNjIyNlpNODIuMjM1OCAxOC44MjMyTDgyLjI0ODUgMTkuNzY5SDgxLjE1MDRDODAuODY2OSAxOS43NjkgODAuNjE3MiAxOS43OTY1IDgwLjQwMTQgMTkuODUxNkM4MC4xODU1IDE5LjkwMjMgODAuMDA1NyAxOS45Nzg1IDc5Ljg2MTggMjAuMDgwMUM3OS43MTc5IDIwLjE4MTYgNzkuNjEgMjAuMzA0NCA3OS41MzgxIDIwLjQ0ODJDNzkuNDY2MSAyMC41OTIxIDc5LjQzMDIgMjAuNzU1IDc5LjQzMDIgMjAuOTM3Qzc5LjQzMDIgMjEuMTE5IDc5LjQ3MjUgMjEuMjg2MSA3OS41NTcxIDIxLjQzODVDNzkuNjQxOCAyMS41ODY2IDc5Ljc2NDUgMjEuNzAzIDc5LjkyNTMgMjEuNzg3NkM4MC4wOTAzIDIxLjg3MjIgODAuMjg5MiAyMS45MTQ2IDgwLjUyMiAyMS45MTQ2QzgwLjgzNTEgMjEuOTE0NiA4MS4xMDgxIDIxLjg1MTEgODEuMzQwOCAyMS43MjQxQzgxLjU3NzggMjEuNTkyOSA4MS43NjQgMjEuNDM0MiA4MS44OTk0IDIxLjI0OEM4Mi4wMzQ4IDIxLjA1NzYgODIuMTA2OCAyMC44Nzc4IDgyLjExNTIgMjAuNzA4NUw4Mi42MTA0IDIxLjM4NzdDODIuNTU5NiAyMS41NjEyIDgyLjQ3MjggMjEuNzQ3NCA4Mi4zNTAxIDIxLjk0NjNDODIuMjI3NCAyMi4xNDUyIDgyLjA2NjYgMjIuMzM1NiA4MS44Njc3IDIyLjUxNzZDODEuNjczIDIyLjY5NTMgODEuNDM4MiAyMi44NDEzIDgxLjE2MzEgMjIuOTU1NkM4MC44OTIzIDIzLjA2OTggODAuNTc5MSAyMy4xMjcgODAuMjIzNiAyMy4xMjdDNzkuNzc1MSAyMy4xMjcgNzkuMzc1MiAyMy4wMzgxIDc5LjAyMzkgMjIuODYwNEM3OC42NzI3IDIyLjY3ODQgNzguMzk3NiAyMi40MzUxIDc4LjE5ODcgMjIuMTMwNEM3Ny45OTk4IDIxLjgyMTUgNzcuOTAwNCAyMS40NzIzIDc3LjkwMDQgMjEuMDgzQzc3LjkwMDQgMjAuNzE5MSA3Ny45NjgxIDIwLjM5NzUgNzguMTAzNSAyMC4xMTgyQzc4LjI0MzIgMTkuODM0NiA3OC40NDYzIDE5LjU5NzcgNzguNzEyOSAxOS40MDcyQzc4Ljk4MzcgMTkuMjE2OCA3OS4zMTM4IDE5LjA3MjkgNzkuNzAzMSAxOC45NzU2QzgwLjA5MjQgMTguODc0IDgwLjUzNjggMTguODIzMiA4MS4wMzYxIDE4LjgyMzJIODIuMjM1OFpNODYuOTM1NSAxNy40Mzk1VjIzSDg1LjQwNThWMTYuMTMxOEg4Ni44NjU3TDg2LjkzNTUgMTcuNDM5NVpNODkuMDM2NiAxNi4wODc0TDg5LjAyMzkgMTcuNTA5M0M4OC45MzA4IDE3LjQ5MjQgODguODI5MyAxNy40Nzk3IDg4LjcxOTIgMTcuNDcxMkM4OC42MTM0IDE3LjQ2MjcgODguNTA3NiAxNy40NTg1IDg4LjQwMTkgMTcuNDU4NUM4OC4xMzk1IDE3LjQ1ODUgODcuOTA4OSAxNy40OTY2IDg3LjcxIDE3LjU3MjhDODcuNTExMSAxNy42NDQ3IDg3LjM0MzkgMTcuNzUwNSA4Ny4yMDg1IDE3Ljg5MDFDODcuMDc3MyAxOC4wMjU2IDg2Ljk3NTcgMTguMTkwNiA4Ni45MDM4IDE4LjM4NTNDODYuODMxOSAxOC41Nzk5IDg2Ljc4OTYgMTguNzk3OSA4Ni43NzY5IDE5LjAzOTFMODYuNDI3NyAxOS4wNjQ1Qzg2LjQyNzcgMTguNjMyOCA4Ni40NzAxIDE4LjIzMjkgODYuNTU0NyAxNy44NjQ3Qzg2LjYzOTMgMTcuNDk2NiA4Ni43NjYzIDE3LjE3MjkgODYuOTM1NSAxNi44OTM2Qzg3LjEwOSAxNi42MTQzIDg3LjMyNDkgMTYuMzk2MyA4Ny41ODMgMTYuMjM5N0M4Ny44NDU0IDE2LjA4MzIgODguMTQ3OSAxNi4wMDQ5IDg4LjQ5MDcgMTYuMDA0OUM4OC41ODM4IDE2LjAwNDkgODguNjgzMyAxNi4wMTMzIDg4Ljc4OTEgMTYuMDMwM0M4OC44OTkxIDE2LjA0NzIgODguOTgxNiAxNi4wNjYyIDg5LjAzNjYgMTYuMDg3NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTE2LjYwNTUgOTcuMTkxNEgxOC4yOTNDMTguOTQ5MiA5Ny4xOTE0IDE5LjQ5MjIgOTcuMDc4MSAxOS45MjE5IDk2Ljg1MTZDMjAuMzU5NCA5Ni42MjUgMjAuNjgzNiA5Ni4zMTI1IDIwLjg5NDUgOTUuOTE0MUMyMS4xMDU1IDk1LjUxNTYgMjEuMjEwOSA5NS4wNTg2IDIxLjIxMDkgOTQuNTQzQzIxLjIxMDkgOTQuMDAzOSAyMS4xMTMzIDkzLjU0MyAyMC45MTggOTMuMTYwMkMyMC43MzA1IDkyLjc2OTUgMjAuNDQxNCA5Mi40Njg4IDIwLjA1MDggOTIuMjU3OEMxOS42NjggOTIuMDQ2OSAxOS4xNzk3IDkxLjk0MTQgMTguNTg1OSA5MS45NDE0QzE4LjA4NTkgOTEuOTQxNCAxNy42MzI4IDkyLjA0MyAxNy4yMjY2IDkyLjI0NjFDMTYuODI4MSA5Mi40NDE0IDE2LjUxMTcgOTIuNzIyNyAxNi4yNzczIDkzLjA4OThDMTYuMDQzIDkzLjQ0OTIgMTUuOTI1OCA5My44Nzg5IDE1LjkyNTggOTQuMzc4OUgxMy4wODk4QzEzLjA4OTggOTMuNDcyNyAxMy4zMjgxIDkyLjY2OCAxMy44MDQ3IDkxLjk2NDhDMTQuMjgxMiA5MS4yNjE3IDE0LjkyOTcgOTAuNzEwOSAxNS43NSA5MC4zMTI1QzE2LjU3ODEgODkuOTA2MiAxNy41MDc4IDg5LjcwMzEgMTguNTM5MSA4OS43MDMxQzE5LjY0MDYgODkuNzAzMSAyMC42MDE2IDg5Ljg4NjcgMjEuNDIxOSA5MC4yNTM5QzIyLjI1IDkwLjYxMzMgMjIuODk0NSA5MS4xNTIzIDIzLjM1NTUgOTEuODcxMUMyMy44MTY0IDkyLjU4OTggMjQuMDQ2OSA5My40ODA1IDI0LjA0NjkgOTQuNTQzQzI0LjA0NjkgOTUuMDI3MyAyMy45MzM2IDk1LjUxOTUgMjMuNzA3IDk2LjAxOTVDMjMuNDgwNSA5Ni41MTk1IDIzLjE0NDUgOTYuOTc2NiAyMi42OTkyIDk3LjM5MDZDMjIuMjUzOSA5Ny43OTY5IDIxLjY5OTIgOTguMTI4OSAyMS4wMzUyIDk4LjM4NjdDMjAuMzcxMSA5OC42MzY3IDE5LjYwMTYgOTguNzYxNyAxOC43MjY2IDk4Ljc2MTdIMTYuNjA1NVY5Ny4xOTE0Wk0xNi42MDU1IDk5LjM5NDVWOTcuODQ3N0gxOC43MjY2QzE5LjcyNjYgOTcuODQ3NyAyMC41NzgxIDk3Ljk2NDggMjEuMjgxMiA5OC4xOTkyQzIxLjk5MjIgOTguNDMzNiAyMi41NzAzIDk4Ljc1NzggMjMuMDE1NiA5OS4xNzE5QzIzLjQ2MDkgOTkuNTc4MSAyMy43ODUyIDEwMC4wNDMgMjMuOTg4MyAxMDAuNTY2QzI0LjE5OTIgMTAxLjA5IDI0LjMwNDcgMTAxLjY0NSAyNC4zMDQ3IDEwMi4yM0MyNC4zMDQ3IDEwMy4wMjcgMjQuMTYwMiAxMDMuNzM4IDIzLjg3MTEgMTA0LjM2M0MyMy41ODk4IDEwNC45OCAyMy4xODc1IDEwNS41MDQgMjIuNjY0MSAxMDUuOTM0QzIyLjE0MDYgMTA2LjM2MyAyMS41MjczIDEwNi42ODggMjAuODI0MiAxMDYuOTA2QzIwLjEyODkgMTA3LjEyNSAxOS4zNzExIDEwNy4yMzQgMTguNTUwOCAxMDcuMjM0QzE3LjgxNjQgMTA3LjIzNCAxNy4xMTMzIDEwNy4xMzMgMTYuNDQxNCAxMDYuOTNDMTUuNzY5NSAxMDYuNzI3IDE1LjE2OCAxMDYuNDI2IDE0LjYzNjcgMTA2LjAyN0MxNC4xMDU1IDEwNS42MjEgMTMuNjgzNiAxMDUuMTE3IDEzLjM3MTEgMTA0LjUxNkMxMy4wNjY0IDEwMy45MDYgMTIuOTE0MSAxMDMuMjAzIDEyLjkxNDEgMTAyLjQwNkgxNS43MzgzQzE1LjczODMgMTAyLjkxNCAxNS44NTU1IDEwMy4zNjMgMTYuMDg5OCAxMDMuNzU0QzE2LjMzMiAxMDQuMTM3IDE2LjY2OCAxMDQuNDM4IDE3LjA5NzcgMTA0LjY1NkMxNy41MzUyIDEwNC44NzUgMTguMDM1MiAxMDQuOTg0IDE4LjU5NzcgMTA0Ljk4NEMxOS4xOTE0IDEwNC45ODQgMTkuNzAzMSAxMDQuODc5IDIwLjEzMjggMTA0LjY2OEMyMC41NjI1IDEwNC40NTcgMjAuODkwNiAxMDQuMTQ1IDIxLjExNzIgMTAzLjczQzIxLjM1MTYgMTAzLjMxNiAyMS40Njg4IDEwMi44MTYgMjEuNDY4OCAxMDIuMjNDMjEuNDY4OCAxMDEuNTY2IDIxLjMzOTggMTAxLjAyNyAyMS4wODIgMTAwLjYxM0MyMC44MjQyIDEwMC4xOTkgMjAuNDU3IDk5Ljg5NDUgMTkuOTgwNSA5OS42OTkyQzE5LjUwMzkgOTkuNDk2MSAxOC45NDE0IDk5LjM5NDUgMTguMjkzIDk5LjM5NDVIMTYuNjA1NVpNMzUuNDYwOSA4OS44MzJIMzUuODEyNVY5Mi4xNDA2SDM1LjYxMzNDMzQuNjA1NSA5Mi4xNDA2IDMzLjczNDQgOTIuMjk2OSAzMyA5Mi42MDk0QzMyLjI3MzQgOTIuOTIxOSAzMS42NzU4IDkzLjM1MTYgMzEuMjA3IDkzLjg5ODRDMzAuNzM4MyA5NC40NDUzIDMwLjM4NjcgOTUuMDg1OSAzMC4xNTIzIDk1LjgyMDNDMjkuOTI1OCA5Ni41NDY5IDI5LjgxMjUgOTcuMzIwMyAyOS44MTI1IDk4LjE0MDZWMTAwLjgyNEMyOS44MTI1IDEwMS41MDQgMjkuODg2NyAxMDIuMTA1IDMwLjAzNTIgMTAyLjYyOUMzMC4xODM2IDEwMy4xNDUgMzAuMzkwNiAxMDMuNTc4IDMwLjY1NjIgMTAzLjkzQzMwLjkyOTcgMTA0LjI3MyAzMS4yNDIyIDEwNC41MzUgMzEuNTkzOCAxMDQuNzE1QzMxLjk0NTMgMTA0Ljg5NSAzMi4zMjQyIDEwNC45ODQgMzIuNzMwNSAxMDQuOTg0QzMzLjE1MjMgMTA0Ljk4NCAzMy41MzUyIDEwNC44OTggMzMuODc4OSAxMDQuNzI3QzM0LjIyMjcgMTA0LjU0NyAzNC41MTU2IDEwNC4zMDEgMzQuNzU3OCAxMDMuOTg4QzM1IDEwMy42NzYgMzUuMTgzNiAxMDMuMzA1IDM1LjMwODYgMTAyLjg3NUMzNS40MzM2IDEwMi40NDUgMzUuNDk2MSAxMDEuOTc3IDM1LjQ5NjEgMTAxLjQ2OUMzNS40OTYxIDEwMC45ODQgMzUuNDMzNiAxMDAuNTMxIDM1LjMwODYgMTAwLjEwOUMzNS4xOTE0IDk5LjY3OTcgMzUuMDE1NiA5OS4zMDQ3IDM0Ljc4MTIgOTguOTg0NEMzNC41NDY5IDk4LjY1NjIgMzQuMjUzOSA5OC40MDIzIDMzLjkwMjMgOTguMjIyN0MzMy41NTg2IDk4LjAzNTIgMzMuMTYwMiA5Ny45NDE0IDMyLjcwNyA5Ny45NDE0QzMyLjE0NDUgOTcuOTQxNCAzMS42MzI4IDk4LjA3NDIgMzEuMTcxOSA5OC4zMzk4QzMwLjcxODggOTguNjA1NSAzMC4zNTE2IDk4Ljk1MzEgMzAuMDcwMyA5OS4zODI4QzI5Ljc5NjkgOTkuODA0NyAyOS42NDg0IDEwMC4yNTQgMjkuNjI1IDEwMC43M0wyOC41NDY5IDEwMC4zNzlDMjguNjA5NCA5OS42NTIzIDI4Ljc2OTUgOTkgMjkuMDI3MyA5OC40MjE5QzI5LjI5MyA5Ny44NDM4IDI5LjY0MDYgOTcuMzUxNiAzMC4wNzAzIDk2Ljk0NTNDMzAuNSA5Ni41MzkxIDMwLjk5NjEgOTYuMjMwNSAzMS41NTg2IDk2LjAxOTVDMzIuMTI4OSA5NS44MDA4IDMyLjc1MzkgOTUuNjkxNCAzMy40MzM2IDk1LjY5MTRDMzQuMjYxNyA5NS42OTE0IDM0Ljk4MDUgOTUuODQ3NyAzNS41ODk4IDk2LjE2MDJDMzYuMTk5MiA5Ni40NzI3IDM2LjcwMzEgOTYuODk4NCAzNy4xMDE2IDk3LjQzNzVDMzcuNTA3OCA5Ny45Njg4IDM3LjgwODYgOTguNTc4MSAzOC4wMDM5IDk5LjI2NTZDMzguMjA3IDk5Ljk0NTMgMzguMzA4NiAxMDAuNjU2IDM4LjMwODYgMTAxLjM5OEMzOC4zMDg2IDEwMi4yMTkgMzguMTgzNiAxMDIuOTg0IDM3LjkzMzYgMTAzLjY5NUMzNy42ODM2IDEwNC4zOTggMzcuMzE2NCAxMDUuMDE2IDM2LjgzMiAxMDUuNTQ3QzM2LjM1NTUgMTA2LjA3OCAzNS43NzM0IDEwNi40OTIgMzUuMDg1OSAxMDYuNzg5QzM0LjQwNjIgMTA3LjA4NiAzMy42MzI4IDEwNy4yMzQgMzIuNzY1NiAxMDcuMjM0QzMxLjg1MTYgMTA3LjIzNCAzMS4wMzUyIDEwNy4wNTkgMzAuMzE2NCAxMDYuNzA3QzI5LjYwNTUgMTA2LjM1NSAyOSAxMDUuODcxIDI4LjUgMTA1LjI1NEMyOC4wMDc4IDEwNC42MzcgMjcuNjMyOCAxMDMuOTI2IDI3LjM3NSAxMDMuMTIxQzI3LjExNzIgMTAyLjMxNiAyNi45ODgzIDEwMS40NjUgMjYuOTg4MyAxMDAuNTY2Vjk5LjM5NDVDMjYuOTg4MyA5OC4wOTc3IDI3LjE1MjMgOTYuODc1IDI3LjQ4MDUgOTUuNzI2NkMyNy44MDg2IDk0LjU3MDMgMjguMzEyNSA5My41NTA4IDI4Ljk5MjIgOTIuNjY4QzI5LjY3OTcgOTEuNzg1MiAzMC41NTg2IDkxLjA5MzggMzEuNjI4OSA5MC41OTM4QzMyLjY5OTIgOTAuMDg1OSAzMy45NzY2IDg5LjgzMiAzNS40NjA5IDg5LjgzMloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTQyLjcxNzggOTkuNDg3M1Y5OC45NjA5QzQyLjcxNzggOTguNTgyNyA0Mi43OTk4IDk4LjIzODYgNDIuOTYzOSA5Ny45Mjg3QzQzLjEyNzkgOTcuNjE4OCA0My4zNjI2IDk3LjM3MDQgNDMuNjY4IDk3LjE4MzZDNDMuOTczMyA5Ni45OTY3IDQ0LjMzNTYgOTYuOTAzMyA0NC43NTQ5IDk2LjkwMzNDNDUuMTgzMyA5Ni45MDMzIDQ1LjU0NzkgOTYuOTk2NyA0NS44NDg2IDk3LjE4MzZDNDYuMTU0IDk3LjM3MDQgNDYuMzg4NyA5Ny42MTg4IDQ2LjU1MjcgOTcuOTI4N0M0Ni43MTY4IDk4LjIzODYgNDYuNzk4OCA5OC41ODI3IDQ2Ljc5ODggOTguOTYwOVY5OS40ODczQzQ2Ljc5ODggOTkuODU2NCA0Ni43MTY4IDEwMC4xOTYgNDYuNTUyNyAxMDAuNTA2QzQ2LjM5MzIgMTAwLjgxNiA0Ni4xNjA4IDEwMS4wNjQgNDUuODU1NSAxMDEuMjUxQzQ1LjU1NDcgMTAxLjQzOCA0NS4xOTI0IDEwMS41MzEgNDQuNzY4NiAxMDEuNTMxQzQ0LjM0NDcgMTAxLjUzMSA0My45Nzc5IDEwMS40MzggNDMuNjY4IDEwMS4yNTFDNDMuMzYyNiAxMDEuMDY0IDQzLjEyNzkgMTAwLjgxNiA0Mi45NjM5IDEwMC41MDZDNDIuNzk5OCAxMDAuMTk2IDQyLjcxNzggOTkuODU2NCA0Mi43MTc4IDk5LjQ4NzNaTTQzLjY2OCA5OC45NjA5Vjk5LjQ4NzNDNDMuNjY4IDk5LjY5NjkgNDMuNzA2NyA5OS44OTUyIDQzLjc4NDIgMTAwLjA4MkM0My44NjYyIDEwMC4yNjkgNDMuOTg5MyAxMDAuNDIyIDQ0LjE1MzMgMTAwLjU0QzQ0LjMxNzQgMTAwLjY1NCA0NC41MjI1IDEwMC43MTEgNDQuNzY4NiAxMDAuNzExQzQ1LjAxNDYgMTAwLjcxMSA0NS4yMTc0IDEwMC42NTQgNDUuMzc3IDEwMC41NEM0NS41MzY1IDEwMC40MjIgNDUuNjU0OSAxMDAuMjY5IDQ1LjczMjQgMTAwLjA4MkM0NS44MDk5IDk5Ljg5NTIgNDUuODQ4NiA5OS42OTY5IDQ1Ljg0ODYgOTkuNDg3M1Y5OC45NjA5QzQ1Ljg0ODYgOTguNzQ2NyA0NS44MDc2IDk4LjU0NjIgNDUuNzI1NiA5OC4zNTk0QzQ1LjY0ODEgOTguMTY4IDQ1LjUyNzMgOTguMDE1MyA0NS4zNjMzIDk3LjkwMTRDNDUuMjAzOCA5Ny43ODI5IDQ1LjAwMSA5Ny43MjM2IDQ0Ljc1NDkgOTcuNzIzNkM0NC41MTMzIDk3LjcyMzYgNDQuMzEwNSA5Ny43ODI5IDQ0LjE0NjUgOTcuOTAxNEM0My45ODcgOTguMDE1MyA0My44NjYyIDk4LjE2OCA0My43ODQyIDk4LjM1OTRDNDMuNzA2NyA5OC41NDYyIDQzLjY2OCA5OC43NDY3IDQzLjY2OCA5OC45NjA5Wk00Ny41NjQ1IDEwNS4wOTNWMTA0LjU2QzQ3LjU2NDUgMTA0LjE4NiA0Ny42NDY1IDEwMy44NDQgNDcuODEwNSAxMDMuNTM0QzQ3Ljk3NDYgMTAzLjIyNCA0OC4yMDkzIDEwMi45NzYgNDguNTE0NiAxMDIuNzg5QzQ4LjgyIDEwMi42MDIgNDkuMTgyMyAxMDIuNTA5IDQ5LjYwMTYgMTAyLjUwOUM1MC4wMjk5IDEwMi41MDkgNTAuMzk0NSAxMDIuNjAyIDUwLjY5NTMgMTAyLjc4OUM1MS4wMDA3IDEwMi45NzYgNTEuMjM1NCAxMDMuMjI0IDUxLjM5OTQgMTAzLjUzNEM1MS41NjM1IDEwMy44NDQgNTEuNjQ1NSAxMDQuMTg2IDUxLjY0NTUgMTA0LjU2VjEwNS4wOTNDNTEuNjQ1NSAxMDUuNDY2IDUxLjU2MzUgMTA1LjgwOCA1MS4zOTk0IDEwNi4xMThDNTEuMjM5OSAxMDYuNDI4IDUxLjAwNzUgMTA2LjY3NiA1MC43MDIxIDEwNi44NjNDNTAuNDAxNCAxMDcuMDUgNTAuMDM5MSAxMDcuMTQ0IDQ5LjYxNTIgMTA3LjE0NEM0OS4xOTE0IDEwNy4xNDQgNDguODI2OCAxMDcuMDUgNDguNTIxNSAxMDYuODYzQzQ4LjIxNjEgMTA2LjY3NiA0Ny45NzkyIDEwNi40MjggNDcuODEwNSAxMDYuMTE4QzQ3LjY0NjUgMTA1LjgwOCA0Ny41NjQ1IDEwNS40NjYgNDcuNTY0NSAxMDUuMDkzWk00OC41MTQ2IDEwNC41NlYxMDUuMDkzQzQ4LjUxNDYgMTA1LjMwMiA0OC41NTM0IDEwNS41MDMgNDguNjMwOSAxMDUuNjk0QzQ4LjcxMjkgMTA1Ljg4MSA0OC44MzU5IDEwNi4wMzQgNDkgMTA2LjE1MkM0OS4xNjQxIDEwNi4yNjYgNDkuMzY5MSAxMDYuMzIzIDQ5LjYxNTIgMTA2LjMyM0M0OS44NjEzIDEwNi4zMjMgNTAuMDY0MSAxMDYuMjY2IDUwLjIyMzYgMTA2LjE1MkM1MC4zODc3IDEwNi4wMzQgNTAuNTA4NSAxMDUuODgxIDUwLjU4NTkgMTA1LjY5NEM1MC42NjM0IDEwNS41MDcgNTAuNzAyMSAxMDUuMzA3IDUwLjcwMjEgMTA1LjA5M1YxMDQuNTZDNTAuNzAyMSAxMDQuMzQ1IDUwLjY2MTEgMTA0LjE0NSA1MC41NzkxIDEwMy45NThDNTAuNTAxNiAxMDMuNzcxIDUwLjM4MDkgMTAzLjYyMSA1MC4yMTY4IDEwMy41MDdDNTAuMDU3MyAxMDMuMzg4IDQ5Ljg1MjIgMTAzLjMyOSA0OS42MDE2IDEwMy4zMjlDNDkuMzYgMTAzLjMyOSA0OS4xNTcyIDEwMy4zODggNDguOTkzMiAxMDMuNTA3QzQ4LjgzMzcgMTAzLjYyMSA0OC43MTI5IDEwMy43NzEgNDguNjMwOSAxMDMuOTU4QzQ4LjU1MzQgMTA0LjE0NSA0OC41MTQ2IDEwNC4zNDUgNDguNTE0NiAxMDQuNTZaTTQ5LjkxNiA5OC40NjE5TDQ1LjA1NTcgMTA2LjI0MUw0NC4zNDQ3IDEwNS43OUw0OS4yMDUxIDk4LjAxMDdMNDkuOTE2IDk4LjQ2MTlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxyZWN0IHg9IjEyIiB5PSIxMjMiIHdpZHRoPSIxNzYiIGhlaWdodD0iNSIgcng9IjIuNSIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC4wNCIvPgo8cmVjdCB3aWR0aD0iNTEiIGhlaWdodD0iNSIgcng9IjIuNSIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgNjMgMTIzKSIgZmlsbD0iIzNGNTJERCIvPgo8cGF0aCBkPSJNMTcuNTU5MSAxMzkuNDY3VjE0MC42NTlDMTcuNTU5MSAxNDEuMyAxNy41MDE4IDE0MS44NDEgMTcuMzg3MiAxNDIuMjgxQzE3LjI3MjYgMTQyLjcyMiAxNy4xMDc5IDE0My4wNzYgMTYuODkzMSAxNDMuMzQ1QzE2LjY3ODIgMTQzLjYxMyAxNi40MTg2IDE0My44MDggMTYuMTE0MyAxNDMuOTNDMTUuODEzNSAxNDQuMDQ4IDE1LjQ3MzMgMTQ0LjEwNyAxNS4wOTM4IDE0NC4xMDdDMTQuNzkzIDE0NC4xMDcgMTQuNTE1NSAxNDQuMDcgMTQuMjYxMiAxNDMuOTk1QzE0LjAwNyAxNDMuOTE5IDEzLjc3NzggMTQzLjc5OSAxMy41NzM3IDE0My42MzVDMTMuMzczMiAxNDMuNDY2IDEzLjIwMTMgMTQzLjI0OCAxMy4wNTgxIDE0Mi45NzlDMTIuOTE0OSAxNDIuNzExIDEyLjgwNTcgMTQyLjM4NSAxMi43MzA1IDE0Mi4wMDJDMTIuNjU1MyAxNDEuNjE5IDEyLjYxNzcgMTQxLjE3MSAxMi42MTc3IDE0MC42NTlWMTM5LjQ2N0MxMi42MTc3IDEzOC44MjYgMTIuNjc1IDEzOC4yODkgMTIuNzg5NiAxMzcuODU1QzEyLjkwNzcgMTM3LjQyMiAxMy4wNzQyIDEzNy4wNzUgMTMuMjg5MSAxMzYuODEzQzEzLjUwMzkgMTM2LjU0OSAxMy43NjE3IDEzNi4zNTkgMTQuMDYyNSAxMzYuMjQ0QzE0LjM2NjkgMTM2LjEzIDE0LjcwNyAxMzYuMDcyIDE1LjA4MyAxMzYuMDcyQzE1LjM4NzQgMTM2LjA3MiAxNS42NjY3IDEzNi4xMSAxNS45MjA5IDEzNi4xODVDMTYuMTc4NyAxMzYuMjU3IDE2LjQwNzkgMTM2LjM3MyAxNi42MDg0IDEzNi41MzRDMTYuODA4OSAxMzYuNjkyIDE2Ljk3OSAxMzYuOTAzIDE3LjExODcgMTM3LjE2OEMxNy4yNjE5IDEzNy40MjkgMTcuMzcxMSAxMzcuNzUgMTcuNDQ2MyAxMzguMTI5QzE3LjUyMTUgMTM4LjUwOSAxNy41NTkxIDEzOC45NTUgMTcuNTU5MSAxMzkuNDY3Wk0xNi41NjAxIDE0MC44MlYxMzkuM0MxNi41NjAxIDEzOC45NDkgMTYuNTM4NiAxMzguNjQxIDE2LjQ5NTYgMTM4LjM3NkMxNi40NTYyIDEzOC4xMDggMTYuMzk3MSAxMzcuODc5IDE2LjMxODQgMTM3LjY4OUMxNi4yMzk2IDEzNy40OTkgMTYuMTM5MyAxMzcuMzQ1IDE2LjAxNzYgMTM3LjIyN0MxNS44OTk0IDEzNy4xMDkgMTUuNzYxNiAxMzcuMDIzIDE1LjYwNCAxMzYuOTY5QzE1LjQ1IDEzNi45MTIgMTUuMjc2NCAxMzYuODgzIDE1LjA4MyAxMzYuODgzQzE0Ljg0NjcgMTM2Ljg4MyAxNC42MzcyIDEzNi45MjggMTQuNDU0NiAxMzcuMDE4QzE0LjI3MiAxMzcuMTA0IDE0LjExOCAxMzcuMjQxIDEzLjk5MjcgMTM3LjQzMUMxMy44NzA5IDEzNy42MjEgMTMuNzc3OCAxMzcuODcgMTMuNzEzNCAxMzguMTc4QzEzLjY0ODkgMTM4LjQ4NiAxMy42MTY3IDEzOC44NiAxMy42MTY3IDEzOS4zVjE0MC44MkMxMy42MTY3IDE0MS4xNzEgMTMuNjM2NCAxNDEuNDgxIDEzLjY3NTggMTQxLjc1QzEzLjcxODggMTQyLjAxOCAxMy43ODE0IDE0Mi4yNTEgMTMuODYzOCAxNDIuNDQ4QzEzLjk0NjEgMTQyLjY0MSAxNC4wNDY0IDE0Mi44IDE0LjE2NDYgMTQyLjkyNkMxNC4yODI3IDE0My4wNTEgMTQuNDE4OCAxNDMuMTQ0IDE0LjU3MjggMTQzLjIwNUMxNC43MzAzIDE0My4yNjIgMTQuOTA0IDE0My4yOTEgMTUuMDkzOCAxNDMuMjkxQzE1LjMzNzIgMTQzLjI5MSAxNS41NTAzIDE0My4yNDQgMTUuNzMyOSAxNDMuMTUxQzE1LjkxNTUgMTQzLjA1OCAxNi4wNjc3IDE0Mi45MTMgMTYuMTg5NSAxNDIuNzE2QzE2LjMxNDggMTQyLjUxNiAxNi40MDc5IDE0Mi4yNiAxNi40Njg4IDE0MS45NDhDMTYuNTI5NiAxNDEuNjMzIDE2LjU2MDEgMTQxLjI1NyAxNi41NjAxIDE0MC44MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTE3MS45MTYgMTM2LjEzN1YxNDRIMTcwLjkyMlYxMzcuMzc3TDE2OC45MTggMTM4LjEwOFYxMzcuMjExTDE3MS43NiAxMzYuMTM3SDE3MS45MTZaTTE3OS45OTcgMTM5LjQ2N1YxNDAuNjU5QzE3OS45OTcgMTQxLjMgMTc5LjkzOSAxNDEuODQxIDE3OS44MjUgMTQyLjI4MUMxNzkuNzEgMTQyLjcyMiAxNzkuNTQ1IDE0My4wNzYgMTc5LjMzMSAxNDMuMzQ1QzE3OS4xMTYgMTQzLjYxMyAxNzguODU2IDE0My44MDggMTc4LjU1MiAxNDMuOTNDMTc4LjI1MSAxNDQuMDQ4IDE3Ny45MTEgMTQ0LjEwNyAxNzcuNTMxIDE0NC4xMDdDMTc3LjIzIDE0NC4xMDcgMTc2Ljk1MyAxNDQuMDcgMTc2LjY5OSAxNDMuOTk1QzE3Ni40NDQgMTQzLjkxOSAxNzYuMjE1IDE0My43OTkgMTc2LjAxMSAxNDMuNjM1QzE3NS44MTEgMTQzLjQ2NiAxNzUuNjM5IDE0My4yNDggMTc1LjQ5NiAxNDIuOTc5QzE3NS4zNTIgMTQyLjcxMSAxNzUuMjQzIDE0Mi4zODUgMTc1LjE2OCAxNDIuMDAyQzE3NS4wOTMgMTQxLjYxOSAxNzUuMDU1IDE0MS4xNzEgMTc1LjA1NSAxNDAuNjU5VjEzOS40NjdDMTc1LjA1NSAxMzguODI2IDE3NS4xMTIgMTM4LjI4OSAxNzUuMjI3IDEzNy44NTVDMTc1LjM0NSAxMzcuNDIyIDE3NS41MTIgMTM3LjA3NSAxNzUuNzI3IDEzNi44MTNDMTc1Ljk0MSAxMzYuNTQ5IDE3Ni4xOTkgMTM2LjM1OSAxNzYuNSAxMzYuMjQ0QzE3Ni44MDQgMTM2LjEzIDE3Ny4xNDUgMTM2LjA3MiAxNzcuNTIxIDEzNi4wNzJDMTc3LjgyNSAxMzYuMDcyIDE3OC4xMDQgMTM2LjExIDE3OC4zNTggMTM2LjE4NUMxNzguNjE2IDEzNi4yNTcgMTc4Ljg0NSAxMzYuMzczIDE3OS4wNDYgMTM2LjUzNEMxNzkuMjQ2IDEzNi42OTIgMTc5LjQxNyAxMzYuOTAzIDE3OS41NTYgMTM3LjE2OEMxNzkuNjk5IDEzNy40MjkgMTc5LjgwOSAxMzcuNzUgMTc5Ljg4NCAxMzguMTI5QzE3OS45NTkgMTM4LjUwOSAxNzkuOTk3IDEzOC45NTUgMTc5Ljk5NyAxMzkuNDY3Wk0xNzguOTk4IDE0MC44MlYxMzkuM0MxNzguOTk4IDEzOC45NDkgMTc4Ljk3NiAxMzguNjQxIDE3OC45MzMgMTM4LjM3NkMxNzguODk0IDEzOC4xMDggMTc4LjgzNSAxMzcuODc5IDE3OC43NTYgMTM3LjY4OUMxNzguNjc3IDEzNy40OTkgMTc4LjU3NyAxMzcuMzQ1IDE3OC40NTUgMTM3LjIyN0MxNzguMzM3IDEzNy4xMDkgMTc4LjE5OSAxMzcuMDIzIDE3OC4wNDIgMTM2Ljk2OUMxNzcuODg4IDEzNi45MTIgMTc3LjcxNCAxMzYuODgzIDE3Ny41MjEgMTM2Ljg4M0MxNzcuMjg0IDEzNi44ODMgMTc3LjA3NSAxMzYuOTI4IDE3Ni44OTIgMTM3LjAxOEMxNzYuNzA5IDEzNy4xMDQgMTc2LjU1NiAxMzcuMjQxIDE3Ni40MyAxMzcuNDMxQzE3Ni4zMDggMTM3LjYyMSAxNzYuMjE1IDEzNy44NyAxNzYuMTUxIDEzOC4xNzhDMTc2LjA4NiAxMzguNDg2IDE3Ni4wNTQgMTM4Ljg2IDE3Ni4wNTQgMTM5LjNWMTQwLjgyQzE3Ni4wNTQgMTQxLjE3MSAxNzYuMDc0IDE0MS40ODEgMTc2LjExMyAxNDEuNzVDMTc2LjE1NiAxNDIuMDE4IDE3Ni4yMTkgMTQyLjI1MSAxNzYuMzAxIDE0Mi40NDhDMTc2LjM4NCAxNDIuNjQxIDE3Ni40ODQgMTQyLjggMTc2LjYwMiAxNDIuOTI2QzE3Ni43MiAxNDMuMDUxIDE3Ni44NTYgMTQzLjE0NCAxNzcuMDEgMTQzLjIwNUMxNzcuMTY4IDE0My4yNjIgMTc3LjM0MSAxNDMuMjkxIDE3Ny41MzEgMTQzLjI5MUMxNzcuNzc1IDE0My4yOTEgMTc3Ljk4OCAxNDMuMjQ0IDE3OC4xNyAxNDMuMTUxQzE3OC4zNTMgMTQzLjA1OCAxNzguNTA1IDE0Mi45MTMgMTc4LjYyNyAxNDIuNzE2QzE3OC43NTIgMTQyLjUxNiAxNzguODQ1IDE0Mi4yNiAxNzguOTA2IDE0MS45NDhDMTc4Ljk2NyAxNDEuNjMzIDE3OC45OTggMTQxLjI1NyAxNzguOTk4IDE0MC44MlpNMTg2LjQzNCAxMzkuNDY3VjE0MC42NTlDMTg2LjQzNCAxNDEuMyAxODYuMzc3IDE0MS44NDEgMTg2LjI2MiAxNDIuMjgxQzE4Ni4xNDggMTQyLjcyMiAxODUuOTgzIDE0My4wNzYgMTg1Ljc2OCAxNDMuMzQ1QzE4NS41NTMgMTQzLjYxMyAxODUuMjk0IDE0My44MDggMTg0Ljk4OSAxNDMuOTNDMTg0LjY4OCAxNDQuMDQ4IDE4NC4zNDggMTQ0LjEwNyAxODMuOTY5IDE0NC4xMDdDMTgzLjY2OCAxNDQuMTA3IDE4My4zOSAxNDQuMDcgMTgzLjEzNiAxNDMuOTk1QzE4Mi44ODIgMTQzLjkxOSAxODIuNjUzIDE0My43OTkgMTgyLjQ0OSAxNDMuNjM1QzE4Mi4yNDggMTQzLjQ2NiAxODIuMDc2IDE0My4yNDggMTgxLjkzMyAxNDIuOTc5QzE4MS43OSAxNDIuNzExIDE4MS42ODEgMTQyLjM4NSAxODEuNjA1IDE0Mi4wMDJDMTgxLjUzIDE0MS42MTkgMTgxLjQ5MyAxNDEuMTcxIDE4MS40OTMgMTQwLjY1OVYxMzkuNDY3QzE4MS40OTMgMTM4LjgyNiAxODEuNTUgMTM4LjI4OSAxODEuNjY1IDEzNy44NTVDMTgxLjc4MyAxMzcuNDIyIDE4MS45NDkgMTM3LjA3NSAxODIuMTY0IDEzNi44MTNDMTgyLjM3OSAxMzYuNTQ5IDE4Mi42MzcgMTM2LjM1OSAxODIuOTM4IDEzNi4yNDRDMTgzLjI0MiAxMzYuMTMgMTgzLjU4MiAxMzYuMDcyIDE4My45NTggMTM2LjA3MkMxODQuMjYyIDEzNi4wNzIgMTg0LjU0MiAxMzYuMTEgMTg0Ljc5NiAxMzYuMTg1QzE4NS4wNTQgMTM2LjI1NyAxODUuMjgzIDEzNi4zNzMgMTg1LjQ4MyAxMzYuNTM0QzE4NS42ODQgMTM2LjY5MiAxODUuODU0IDEzNi45MDMgMTg1Ljk5NCAxMzcuMTY4QzE4Ni4xMzcgMTM3LjQyOSAxODYuMjQ2IDEzNy43NSAxODYuMzIxIDEzOC4xMjlDMTg2LjM5NiAxMzguNTA5IDE4Ni40MzQgMTM4Ljk1NSAxODYuNDM0IDEzOS40NjdaTTE4NS40MzUgMTQwLjgyVjEzOS4zQzE4NS40MzUgMTM4Ljk0OSAxODUuNDE0IDEzOC42NDEgMTg1LjM3MSAxMzguMzc2QzE4NS4zMzEgMTM4LjEwOCAxODUuMjcyIDEzNy44NzkgMTg1LjE5MyAxMzcuNjg5QzE4NS4xMTUgMTM3LjQ5OSAxODUuMDE0IDEzNy4zNDUgMTg0Ljg5MyAxMzcuMjI3QzE4NC43NzQgMTM3LjEwOSAxODQuNjM3IDEzNy4wMjMgMTg0LjQ3OSAxMzYuOTY5QzE4NC4zMjUgMTM2LjkxMiAxODQuMTUxIDEzNi44ODMgMTgzLjk1OCAxMzYuODgzQzE4My43MjIgMTM2Ljg4MyAxODMuNTEyIDEzNi45MjggMTgzLjMzIDEzNy4wMThDMTgzLjE0NyAxMzcuMTA0IDE4Mi45OTMgMTM3LjI0MSAxODIuODY4IDEzNy40MzFDMTgyLjc0NiAxMzcuNjIxIDE4Mi42NTMgMTM3Ljg3IDE4Mi41ODggMTM4LjE3OEMxODIuNTI0IDEzOC40ODYgMTgyLjQ5MiAxMzguODYgMTgyLjQ5MiAxMzkuM1YxNDAuODJDMTgyLjQ5MiAxNDEuMTcxIDE4Mi41MTEgMTQxLjQ4MSAxODIuNTUxIDE0MS43NUMxODIuNTk0IDE0Mi4wMTggMTgyLjY1NiAxNDIuMjUxIDE4Mi43MzkgMTQyLjQ0OEMxODIuODIxIDE0Mi42NDEgMTgyLjkyMSAxNDIuOCAxODMuMDQgMTQyLjkyNkMxODMuMTU4IDE0My4wNTEgMTgzLjI5NCAxNDMuMTQ0IDE4My40NDggMTQzLjIwNUMxODMuNjA1IDE0My4yNjIgMTgzLjc3OSAxNDMuMjkxIDE4My45NjkgMTQzLjI5MUMxODQuMjEyIDE0My4yOTEgMTg0LjQyNSAxNDMuMjQ0IDE4NC42MDggMTQzLjE1MUMxODQuNzkxIDE0My4wNTggMTg0Ljk0MyAxNDIuOTEzIDE4NS4wNjQgMTQyLjcxNkMxODUuMTkgMTQyLjUxNiAxODUuMjgzIDE0Mi4yNiAxODUuMzQ0IDE0MS45NDhDMTg1LjQwNSAxNDEuNjMzIDE4NS40MzUgMTQxLjI1NyAxODUuNDM1IDE0MC44MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPC9nPgo8L2c+CjxyZWN0IHg9IjAuNSIgeT0iMC41IiB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgcng9IjMuNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZF80Njg1XzQwOTg5IiB4PSItOCIgeT0iLTQiIHdpZHRoPSIyMTYiIGhlaWdodD0iMTc2IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjQiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNCIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDQgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180Njg1XzQwOTg5Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2ODVfNDA5ODkiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfYl80Njg1XzQwOTg5IiB4PSI2IiB5PSI2IiB3aWR0aD0iMTg4IiBoZWlnaHQ9IjE0OCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVHYXVzc2lhbkJsdXIgaW49IkJhY2tncm91bmRJbWFnZUZpeCIgc3RkRGV2aWF0aW9uPSIzIi8+CjxmZUNvbXBvc2l0ZSBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iaW4iIHJlc3VsdD0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80Njg1XzQwOTg5Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9iYWNrZ3JvdW5kQmx1cl80Njg1XzQwOTg5IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzQ2ODVfNDA5ODkiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjQiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==", "description": "Displays any value reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { "type": "latest", @@ -19,7 +19,6 @@ "basicModeDirective": "tb-progress-bar-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Progress bar\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" }, - "externalId": null, "tags": [ "progress", "loading", diff --git a/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json index e19ff918be..a17d2ee031 100644 --- a/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json @@ -2,7 +2,7 @@ "fqn": "simple_value_and_chart_card", "name": "Simple Value and chart card", "deprecated": false, - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAflBMVEUAAADg4ODg4ODf39/////g4OAhISHj4+M/Ut09PT2srKx0dHSQkJDn6fvHx8fx8fEvLy9XaOHP1PdYWFi3vvKenp7z9P3V1dW6urqCgoKHk+pvfeVLXd/b3/lmZmaTnuyrs/DDyfWfqO5jc+Orq6tLS0ufqe5LXN97iOhKSkrA29V9AAAABHRSTlMA77cggcfpsAAABRRJREFUeNrsz0EBACEIADBBedu/7RlDua3BBgAAAAAAwO/MjHpdzvOIvZ63Y45s8DiTHLVaiC6RErmMyG0+9stdRXIYiKLJ5RZ6IBUIITlx1A7m/39wq3A3y/ZqN5jA0/M4ILnqWhYc5MD+EXk1vqZIlTv4fPwhUnin4B1IHLie9YnEGFVtEryDwozLWYs4IZxOPUcAJVoxIT0LIFGmpY5dzyB2oOQ8feY+MWIFZrQxRxZPu2DNJSKiVB5AYgjkzkAdiAyqbEA9LAjiAQOyr8nYlNqsKUAj4EsLEpWeLLhCxGdBZkeyIeSwZkPkXtFYsHmqzUWK1JBgaT1frd8i3KRGi0QDVlwiMthEBpOJyBmJNZERmGZ0qIgcegZAHaVRnkV8n51FxPdYcYVIIZ22EPGCJ3eRrhp0LdLofKDIYBJnIVK4QYM4jyAB6V8nMsTAkitEoCpArM8iGyyZNm5AGadIZAbaQ6RbVMNDJPsTMrHiGpEbQwuczyJsB70JXmynSLVGlQOVemDQu4dIfbOF+oYL+Eukdziy7VsBZq5nVPNEZN92DyB9Tzdg5OGNhV7htifL7M7Mj3086xX/5fqPRj+Al+VbipRU8LJ8zf+Rz8wv9ukYBWIYBqJoO40KCePCjhBJYe/9T7ikCCHJBTTg32m6B/aCZGtBsrUg2VqQbOWD+L5bpYdIbDjrhRsS2KK426HoQgxpiOtRlalCCwkc9yGqlRRip+POEaSQrs97QCghjvIc6vxRQtrnTwwYIUTQ3lOdnRDSIJ9twOggf3bNqElVEAzDNwx+CpggUimW5s457f//g0eWU5gszTi7hRc+F/lmTuMz34eQZgvil2T1IkJeeuZfovySrFuk02ComZv+bEH8kqxBhAnRBsZD0zPVnrnubx6afW8MIraIumgY4Z0KL0UYh7odNxd9M/LgTWQRxqHphJBX8BZ/0i1F1BkMT5a6EkRUEca5sElwzuYfTd/1l0vXojADVzFFrq7n2d/GmzAW0MIwNVFCyp69TUSA9K+hrrEWIV1zKtFoMAzyTSI1D11DGR/QQgSHq2wZ688a9LlnTHQD8At7tYg/TXfQuZbXbPnX/eFg0LW4y9VuFgqIHI6E5jbuMpcNFcmQJafkmCVPuqFFU5pbO6nGOi2m7aUU6sHOqIigSJJiQ7lzmZhsOOJPTExISvwFRQGGWfuoGngnTG/o7jfvuHCoAyK7FBc0I+YcjUda2WwdcYUo3lnBE6XjaxX+iTRDDjCiG4Z+FdkFRDL7d4sSF4/ZsMc5ys2u0S23z9CLXXBF7sGEbBV6Db7ICX+aTYWxyaXJdMyWNCVpOm4LTOxoskI+vEZvxm+tJLGjIb1nMmZLQlIy7soxPtiDKd3/YFXxShE34sk8OzJbiDCcowh4IrYIySx74yhMBxK9k7AItb3j8iIRAVcUASPie9CH7Ivsn9WDMxQBX+Rjcu6Vy47DvUh5Pi+NaKBRKAK+yB7j4+SUKfLY3Y7I522nzsB7FANfZF9gMsseJS7shXk2WKSGc5xy+CKJWXdkhvwrZ//zA0mB0yrPysd6tQ00LYqFv0S5QRCd5Ef2qd1/nN5qAN2hWPgiObnxgQ6TPCP5OJWn46RQPf95V63hqa6EQaCFrFKESRSdtdzE3kQ2kbWyiayNTWRtbCJrYxNZG//auQMaAEAYiIFZwvxbZjLgd+egBirkNUEhdSJ0znInZoI0W6r+Xc2WCgAAAAAAYJsLnGVmvd/WiIIAAAAASUVORK5CYII=", + "image": "tb-image:c2ltcGxlX3ZhbHVlX2FuZF9jaGFydF9jYXJkX3N5c3RlbV93aWRnZXRfaW1hZ2UucG5n:IlNpbXBsZSBWYWx1ZSBhbmQgY2hhcnQgY2FyZCIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHk9IjMyIiB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijk2IiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iMC41IiB5PSIzMi41IiB3aWR0aD0iMTk5IiBoZWlnaHQ9Ijk1IiByeD0iMy41IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMTYuNzQxNyA0NS43NTc4VjU1SDE1LjE2MTFWNDUuNzU3OEgxNi43NDE3Wk0xOS42NDI2IDQ1Ljc1NzhWNDcuMDI3M0gxMi4yODU2VjQ1Ljc1NzhIMTkuNjQyNlpNMjMuMzc3NCA1NS4xMjdDMjIuODY5NiA1NS4xMjcgMjIuNDEwNSA1NS4wNDQ0IDIyIDU0Ljg3OTRDMjEuNTkzOCA1NC43MTAxIDIxLjI0NjcgNTQuNDc1MyAyMC45NTkgNTQuMTc0OEMyMC42NzU1IDUzLjg3NDMgMjAuNDU3NSA1My41MjEgMjAuMzA1MiA1My4xMTQ3QzIwLjE1MjggNTIuNzA4NSAyMC4wNzY3IDUyLjI3MDUgMjAuMDc2NyA1MS44MDA4VjUxLjU0NjlDMjAuMDc2NyA1MS4wMDk0IDIwLjE1NDkgNTAuNTIyOCAyMC4zMTE1IDUwLjA4NjlDMjAuNDY4MSA0OS42NTEgMjAuNjg2IDQ5LjI3ODYgMjAuOTY1MyA0OC45Njk3QzIxLjI0NDYgNDguNjU2NiAyMS41NzQ3IDQ4LjQxNzUgMjEuOTU1NiA0OC4yNTI0QzIyLjMzNjQgNDguMDg3NCAyMi43NDkgNDguMDA0OSAyMy4xOTM0IDQ4LjAwNDlDMjMuNjg0MiA0OC4wMDQ5IDI0LjExMzggNDguMDg3NCAyNC40ODE5IDQ4LjI1MjRDMjQuODUwMSA0OC40MTc1IDI1LjE1NDggNDguNjUwMiAyNS4zOTYgNDguOTUwN0MyNS42NDE0IDQ5LjI0NjkgMjUuODIzNCA0OS42MDAzIDI1Ljk0MTkgNTAuMDEwN0MyNi4wNjQ2IDUwLjQyMTIgMjYuMTI2IDUwLjg3NCAyNi4xMjYgNTEuMzY5MVY1Mi4wMjI5SDIwLjgxOTNWNTAuOTI0OEgyNC42MTUyVjUwLjgwNDJDMjQuNjA2OCA1MC41MjkxIDI0LjU1MTggNTAuMjcxIDI0LjQ1MDIgNTAuMDI5OEMyNC4zNTI5IDQ5Ljc4ODYgMjQuMjAyNiA0OS41OTM5IDIzLjk5OTUgNDkuNDQ1OEMyMy43OTY0IDQ5LjI5NzcgMjMuNTI1NiA0OS4yMjM2IDIzLjE4NyA0OS4yMjM2QzIyLjkzMzEgNDkuMjIzNiAyMi43MDY3IDQ5LjI3ODYgMjIuNTA3OCA0OS4zODg3QzIyLjMxMzIgNDkuNDk0NSAyMi4xNTAyIDQ5LjY0ODkgMjIuMDE5IDQ5Ljg1MjFDMjEuODg3OSA1MC4wNTUyIDIxLjc4NjMgNTAuMzAwNiAyMS43MTQ0IDUwLjU4ODRDMjEuNjQ2NiA1MC44NzE5IDIxLjYxMjggNTEuMTkxNCAyMS42MTI4IDUxLjU0NjlWNTEuODAwOEMyMS42MTI4IDUyLjEwMTIgMjEuNjUzIDUyLjM4MDUgMjEuNzMzNCA1Mi42Mzg3QzIxLjgxOCA1Mi44OTI2IDIxLjk0MDggNTMuMTE0NyAyMi4xMDE2IDUzLjMwNTJDMjIuMjYyNCA1My40OTU2IDIyLjQ1NyA1My42NDU4IDIyLjY4NTUgNTMuNzU1OUMyMi45MTQxIDUzLjg2MTcgMjMuMTc0MyA1My45MTQ2IDIzLjQ2NjMgNTMuOTE0NkMyMy44MzQ1IDUzLjkxNDYgMjQuMTYyNCA1My44NDA1IDI0LjQ1MDIgNTMuNjkyNEMyNC43MzggNTMuNTQ0MyAyNC45ODc2IDUzLjMzNDggMjUuMTk5MiA1My4wNjRMMjYuMDA1NCA1My44NDQ3QzI1Ljg1NzMgNTQuMDYwNSAyNS42NjQ3IDU0LjI2NzkgMjUuNDI3NyA1NC40NjY4QzI1LjE5MDggNTQuNjYxNSAyNC45MDA5IDU0LjgyMDEgMjQuNTU4MSA1NC45NDI5QzI0LjIxOTYgNTUuMDY1NiAyMy44MjYgNTUuMTI3IDIzLjM3NzQgNTUuMTI3Wk0yOS4wOTkxIDQ5LjUyODNWNTVIMjcuNTY5M1Y0OC4xMzE4SDI5LjAxMDNMMjkuMDk5MSA0OS41MjgzWk0yOC44NTE2IDUxLjMxMkwyOC4zMzExIDUxLjMwNTdDMjguMzMxMSA1MC44MzE3IDI4LjM5MDMgNTAuMzkzNyAyOC41MDg4IDQ5Ljk5MTdDMjguNjI3MyA0OS41ODk3IDI4LjgwMDggNDkuMjQwNiAyOS4wMjkzIDQ4Ljk0NDNDMjkuMjU3OCA0OC42NDM5IDI5LjU0MTMgNDguNDEzMiAyOS44Nzk5IDQ4LjI1MjRDMzAuMjIyNyA0OC4wODc0IDMwLjYxODMgNDguMDA0OSAzMS4wNjY5IDQ4LjAwNDlDMzEuMzggNDguMDA0OSAzMS42NjU3IDQ4LjA1MTQgMzEuOTIzOCA0OC4xNDQ1QzMyLjE4NjIgNDguMjMzNCAzMi40MTI2IDQ4LjM3NTIgMzIuNjAzIDQ4LjU2OThDMzIuNzk3NyA0OC43NjQ1IDMyLjk0NTggNDkuMDE0MiAzMy4wNDc0IDQ5LjMxODhDMzMuMTUzMiA0OS42MjM1IDMzLjIwNjEgNDkuOTkxNyAzMy4yMDYxIDUwLjQyMzNWNTVIMzEuNjc2M1Y1MC41NTY2QzMxLjY3NjMgNTAuMjIyMyAzMS42MjU1IDQ5Ljk2IDMxLjUyMzkgNDkuNzY5NUMzMS40MjY2IDQ5LjU3OTEgMzEuMjg0OCA0OS40NDM3IDMxLjA5ODYgNDkuMzYzM0MzMC45MTY3IDQ5LjI3ODYgMzAuNjk4NyA0OS4yMzYzIDMwLjQ0NDggNDkuMjM2M0MzMC4xNTcxIDQ5LjIzNjMgMjkuOTExNiA0OS4yOTEzIDI5LjcwODUgNDkuNDAxNEMyOS41MDk2IDQ5LjUxMTQgMjkuMzQ2NyA0OS42NjE2IDI5LjIxOTcgNDkuODUyMUMyOS4wOTI4IDUwLjA0MjUgMjguOTk5NyA1MC4yNjI1IDI4Ljk0MDQgNTAuNTEyMkMyOC44ODEyIDUwLjc2MTkgMjguODUxNiA1MS4wMjg1IDI4Ljg1MTYgNTEuMzEyWk0zMy4xMTA4IDUwLjkwNThMMzIuMzkzNiA1MS4wNjQ1QzMyLjM5MzYgNTAuNjQ5NyAzMi40NTA3IDUwLjI1ODMgMzIuNTY0OSA0OS44OTAxQzMyLjY4MzQgNDkuNTE3NyAzMi44NTQ4IDQ5LjE5MTkgMzMuMDc5MSA0OC45MTI2QzMzLjMwNzYgNDguNjI5MSAzMy41ODkgNDguNDA2OSAzMy45MjMzIDQ4LjI0NjFDMzQuMjU3NiA0OC4wODUzIDM0LjY0MDYgNDguMDA0OSAzNS4wNzIzIDQ4LjAwNDlDMzUuNDIzNSA0OC4wMDQ5IDM1LjczNjcgNDguMDUzNSAzNi4wMTE3IDQ4LjE1MDlDMzYuMjkxIDQ4LjI0NCAzNi41MjggNDguMzkyMSAzNi43MjI3IDQ4LjU5NTJDMzYuOTE3MyA0OC43OTgzIDM3LjA2NTQgNDkuMDYyOCAzNy4xNjcgNDkuMzg4N0MzNy4yNjg2IDQ5LjcxMDMgMzcuMzE5MyA1MC4wOTk2IDM3LjMxOTMgNTAuNTU2NlY1NUgzNS43ODMyVjUwLjU1MDNDMzUuNzgzMiA1MC4yMDMzIDM1LjczMjQgNDkuOTM0NiAzNS42MzA5IDQ5Ljc0NDFDMzUuNTMzNSA0OS41NTM3IDM1LjM5MzkgNDkuNDIyNSAzNS4yMTE5IDQ5LjM1MDZDMzUuMDI5OSA0OS4yNzQ0IDM0LjgxMiA0OS4yMzYzIDM0LjU1ODEgNDkuMjM2M0MzNC4zMjExIDQ5LjIzNjMgMzQuMTExNyA0OS4yODA4IDMzLjkyOTcgNDkuMzY5NkMzMy43NTIgNDkuNDU0MyAzMy42MDE3IDQ5LjU3NDkgMzMuNDc5IDQ5LjczMTRDMzMuMzU2MyA0OS44ODM4IDMzLjI2MzIgNTAuMDU5NCAzMy4xOTk3IDUwLjI1ODNDMzMuMTQwNSA1MC40NTcyIDMzLjExMDggNTAuNjczIDMzLjExMDggNTAuOTA1OFpNNDAuNjc5NyA0OS40NTIxVjU3LjY0MDZIMzkuMTQ5OVY0OC4xMzE4SDQwLjU1OTFMNDAuNjc5NyA0OS40NTIxWk00NS4xNTQ4IDUxLjUwMjRWNTEuNjM1N0M0NS4xNTQ4IDUyLjEzNTEgNDUuMDk1NSA1Mi41OTg1IDQ0Ljk3NzEgNTMuMDI1OUM0NC44NjI4IDUzLjQ0OTEgNDQuNjkxNCA1My44MTkzIDQ0LjQ2MjkgNTQuMTM2N0M0NC4yMzg2IDU0LjQ0OTkgNDMuOTYxNCA1NC42OTMyIDQzLjYzMTMgNTQuODY2N0M0My4zMDEzIDU1LjA0MDIgNDIuOTIwNCA1NS4xMjcgNDIuNDg4OCA1NS4xMjdDNDIuMDYxNCA1NS4xMjcgNDEuNjg2OCA1NS4wNDg3IDQxLjM2NTIgNTQuODkyMUM0MS4wNDc5IDU0LjczMTMgNDAuNzc5MSA1NC41MDQ5IDQwLjU1OTEgNTQuMjEyOUM0MC4zMzkgNTMuOTIwOSA0MC4xNjEzIDUzLjU3ODEgNDAuMDI1OSA1My4xODQ2QzM5Ljg5NDcgNTIuNzg2OCAzOS44MDE2IDUyLjM1MDkgMzkuNzQ2NiA1MS44NzdWNTEuMzYyOEMzOS44MDE2IDUwLjg1OTIgMzkuODk0NyA1MC40MDIyIDQwLjAyNTkgNDkuOTkxN0M0MC4xNjEzIDQ5LjU4MTIgNDAuMzM5IDQ5LjIyNzkgNDAuNTU5MSA0OC45MzE2QzQwLjc3OTEgNDguNjM1NCA0MS4wNDc5IDQ4LjQwNjkgNDEuMzY1MiA0OC4yNDYxQzQxLjY4MjYgNDguMDg1MyA0Mi4wNTI5IDQ4LjAwNDkgNDIuNDc2MSA0OC4wMDQ5QzQyLjkwNzcgNDguMDA0OSA0My4yOTA3IDQ4LjA4OTUgNDMuNjI1IDQ4LjI1ODhDNDMuOTU5MyA0OC40MjM4IDQ0LjI0MDcgNDguNjYwOCA0NC40NjkyIDQ4Ljk2OTdDNDQuNjk3OCA0OS4yNzQ0IDQ0Ljg2OTEgNDkuNjQyNiA0NC45ODM0IDUwLjA3NDJDNDUuMDk3NyA1MC41MDE2IDQ1LjE1NDggNTAuOTc3NyA0NS4xNTQ4IDUxLjUwMjRaTTQzLjYyNSA1MS42MzU3VjUxLjUwMjRDNDMuNjI1IDUxLjE4NTEgNDMuNTk1NCA1MC44OTEgNDMuNTM2MSA1MC42MjAxQzQzLjQ3NjkgNTAuMzQ1MSA0My4zODM4IDUwLjEwMzggNDMuMjU2OCA0OS44OTY1QzQzLjEyOTkgNDkuNjg5MSA0Mi45NjcgNDkuNTI4MyA0Mi43NjgxIDQ5LjQxNDFDNDIuNTczNCA0OS4yOTU2IDQyLjMzODUgNDkuMjM2MyA0Mi4wNjM1IDQ5LjIzNjNDNDEuNzkyNiA0OS4yMzYzIDQxLjU1OTkgNDkuMjgyOSA0MS4zNjUyIDQ5LjM3NkM0MS4xNzA2IDQ5LjQ2NDggNDEuMDA3NiA0OS41ODk3IDQwLjg3NjUgNDkuNzUwNUM0MC43NDUzIDQ5LjkxMTMgNDAuNjQzNyA1MC4wOTk2IDQwLjU3MTggNTAuMzE1NEM0MC40OTk4IDUwLjUyNyA0MC40NDkxIDUwLjc1NzYgNDAuNDE5NCA1MS4wMDczVjUyLjIzODhDNDAuNDcwMiA1Mi41NDM1IDQwLjU1NyA1Mi44MjI4IDQwLjY3OTcgNTMuMDc2N0M0MC44MDI0IDUzLjMzMDYgNDAuOTc1OSA1My41MzM3IDQxLjIwMDIgNTMuNjg2QzQxLjQyODcgNTMuODM0MSA0MS43MjA3IDUzLjkwODIgNDIuMDc2MiA1My45MDgyQzQyLjM1MTIgNTMuOTA4MiA0Mi41ODYxIDUzLjg0OSA0Mi43ODA4IDUzLjczMDVDNDIuOTc1NCA1My42MTIgNDMuMTM0MSA1My40NDkxIDQzLjI1NjggNTMuMjQxN0M0My4zODM4IDUzLjAzMDEgNDMuNDc2OSA1Mi43ODY4IDQzLjUzNjEgNTIuNTExN0M0My41OTU0IDUyLjIzNjcgNDMuNjI1IDUxLjk0NDcgNDMuNjI1IDUxLjYzNTdaTTQ5LjczMzkgNTUuMTI3QzQ5LjIyNjEgNTUuMTI3IDQ4Ljc2NjkgNTUuMDQ0NCA0OC4zNTY0IDU0Ljg3OTRDNDcuOTUwMiA1NC43MTAxIDQ3LjYwMzIgNTQuNDc1MyA0Ny4zMTU0IDU0LjE3NDhDNDcuMDMxOSA1My44NzQzIDQ2LjgxNCA1My41MjEgNDYuNjYxNiA1My4xMTQ3QzQ2LjUwOTMgNTIuNzA4NSA0Ni40MzMxIDUyLjI3MDUgNDYuNDMzMSA1MS44MDA4VjUxLjU0NjlDNDYuNDMzMSA1MS4wMDk0IDQ2LjUxMTQgNTAuNTIyOCA0Ni42NjggNTAuMDg2OUM0Ni44MjQ1IDQ5LjY1MSA0Ny4wNDI1IDQ5LjI3ODYgNDcuMzIxOCA0OC45Njk3QzQ3LjYwMTEgNDguNjU2NiA0Ny45MzEyIDQ4LjQxNzUgNDguMzEyIDQ4LjI1MjRDNDguNjkyOSA0OC4wODc0IDQ5LjEwNTUgNDguMDA0OSA0OS41NDk4IDQ4LjAwNDlDNTAuMDQwNyA0OC4wMDQ5IDUwLjQ3MDIgNDguMDg3NCA1MC44Mzg0IDQ4LjI1MjRDNTEuMjA2NSA0OC40MTc1IDUxLjUxMTIgNDguNjUwMiA1MS43NTI0IDQ4Ljk1MDdDNTEuOTk3OSA0OS4yNDY5IDUyLjE3OTkgNDkuNjAwMyA1Mi4yOTgzIDUwLjAxMDdDNTIuNDIxMSA1MC40MjEyIDUyLjQ4MjQgNTAuODc0IDUyLjQ4MjQgNTEuMzY5MVY1Mi4wMjI5SDQ3LjE3NThWNTAuOTI0OEg1MC45NzE3VjUwLjgwNDJDNTAuOTYzMiA1MC41MjkxIDUwLjkwODIgNTAuMjcxIDUwLjgwNjYgNTAuMDI5OEM1MC43MDkzIDQ5Ljc4ODYgNTAuNTU5MSA0OS41OTM5IDUwLjM1NiA0OS40NDU4QzUwLjE1MjggNDkuMjk3NyA0OS44ODIgNDkuMjIzNiA0OS41NDM1IDQ5LjIyMzZDNDkuMjg5NiA0OS4yMjM2IDQ5LjA2MzIgNDkuMjc4NiA0OC44NjQzIDQ5LjM4ODdDNDguNjY5NiA0OS40OTQ1IDQ4LjUwNjcgNDkuNjQ4OSA0OC4zNzU1IDQ5Ljg1MjFDNDguMjQ0MyA1MC4wNTUyIDQ4LjE0MjcgNTAuMzAwNiA0OC4wNzA4IDUwLjU4ODRDNDguMDAzMSA1MC44NzE5IDQ3Ljk2OTIgNTEuMTkxNCA0Ny45NjkyIDUxLjU0NjlWNTEuODAwOEM0Ny45NjkyIDUyLjEwMTIgNDguMDA5NCA1Mi4zODA1IDQ4LjA4OTggNTIuNjM4N0M0OC4xNzQ1IDUyLjg5MjYgNDguMjk3MiA1My4xMTQ3IDQ4LjQ1OCA1My4zMDUyQzQ4LjYxODggNTMuNDk1NiA0OC44MTM1IDUzLjY0NTggNDkuMDQyIDUzLjc1NTlDNDkuMjcwNSA1My44NjE3IDQ5LjUzMDggNTMuOTE0NiA0OS44MjI4IDUzLjkxNDZDNTAuMTkwOSA1My45MTQ2IDUwLjUxODkgNTMuODQwNSA1MC44MDY2IDUzLjY5MjRDNTEuMDk0NCA1My41NDQzIDUxLjM0NDEgNTMuMzM0OCA1MS41NTU3IDUzLjA2NEw1Mi4zNjE4IDUzLjg0NDdDNTIuMjEzNyA1NC4wNjA1IDUyLjAyMTIgNTQuMjY3OSA1MS43ODQyIDU0LjQ2NjhDNTEuNTQ3MiA1NC42NjE1IDUxLjI1NzMgNTQuODIwMSA1MC45MTQ2IDU0Ljk0MjlDNTAuNTc2IDU1LjA2NTYgNTAuMTgyNSA1NS4xMjcgNDkuNzMzOSA1NS4xMjdaTTU1LjQ2MTkgNDkuNDM5NVY1NUg1My45MzIxVjQ4LjEzMThINTUuMzkyMUw1NS40NjE5IDQ5LjQzOTVaTTU3LjU2MyA0OC4wODc0TDU3LjU1MDMgNDkuNTA5M0M1Ny40NTcyIDQ5LjQ5MjQgNTcuMzU1NiA0OS40Nzk3IDU3LjI0NTYgNDkuNDcxMkM1Ny4xMzk4IDQ5LjQ2MjcgNTcuMDM0IDQ5LjQ1ODUgNTYuOTI4MiA0OS40NTg1QzU2LjY2NTkgNDkuNDU4NSA1Ni40MzUyIDQ5LjQ5NjYgNTYuMjM2MyA0OS41NzI4QzU2LjAzNzQgNDkuNjQ0NyA1NS44NzAzIDQ5Ljc1MDUgNTUuNzM0OSA0OS44OTAxQzU1LjYwMzcgNTAuMDI1NiA1NS41MDIxIDUwLjE5MDYgNTUuNDMwMiA1MC4zODUzQzU1LjM1ODIgNTAuNTc5OSA1NS4zMTU5IDUwLjc5NzkgNTUuMzAzMiA1MS4wMzkxTDU0Ljk1NDEgNTEuMDY0NUM1NC45NTQxIDUwLjYzMjggNTQuOTk2NCA1MC4yMzI5IDU1LjA4MTEgNDkuODY0N0M1NS4xNjU3IDQ5LjQ5NjYgNTUuMjkyNiA0OS4xNzI5IDU1LjQ2MTkgNDguODkzNkM1NS42MzU0IDQ4LjYxNDMgNTUuODUxMiA0OC4zOTYzIDU2LjEwOTQgNDguMjM5N0M1Ni4zNzE3IDQ4LjA4MzIgNTYuNjc0MyA0OC4wMDQ5IDU3LjAxNzEgNDguMDA0OUM1Ny4xMTAyIDQ4LjAwNDkgNTcuMjA5NiA0OC4wMTMzIDU3LjMxNTQgNDguMDMwM0M1Ny40MjU1IDQ4LjA0NzIgNTcuNTA4IDQ4LjA2NjIgNTcuNTYzIDQ4LjA4NzRaTTYyLjQxNSA1My42MjI2VjUwLjM0NzJDNjIuNDE1IDUwLjEwMTcgNjIuMzcwNiA0OS44OTAxIDYyLjI4MTcgNDkuNzEyNEM2Mi4xOTI5IDQ5LjUzNDcgNjIuMDU3NSA0OS4zOTcxIDYxLjg3NTUgNDkuMjk5OEM2MS42OTc4IDQ5LjIwMjUgNjEuNDczNSA0OS4xNTM4IDYxLjIwMjYgNDkuMTUzOEM2MC45NTMgNDkuMTUzOCA2MC43MzcxIDQ5LjE5NjEgNjAuNTU1MiA0OS4yODA4QzYwLjM3MzIgNDkuMzY1NCA2MC4yMzE0IDQ5LjQ3OTcgNjAuMTI5OSA0OS42MjM1QzYwLjAyODMgNDkuNzY3NCA1OS45Nzc1IDQ5LjkzMDMgNTkuOTc3NSA1MC4xMTIzSDU4LjQ1NDFDNTguNDU0MSA0OS44NDE1IDU4LjUxOTcgNDkuNTc5MSA1OC42NTA5IDQ5LjMyNTJDNTguNzgyMSA0OS4wNzEzIDU4Ljk3MjUgNDguODQ0OSA1OS4yMjIyIDQ4LjY0NkM1OS40NzE4IDQ4LjQ0NzEgNTkuNzcwMiA0OC4yOTA1IDYwLjExNzIgNDguMTc2M0M2MC40NjQyIDQ4LjA2MiA2MC44NTM1IDQ4LjAwNDkgNjEuMjg1MiA0OC4wMDQ5QzYxLjgwMTQgNDguMDA0OSA2Mi4yNTg1IDQ4LjA5MTYgNjIuNjU2MiA0OC4yNjUxQzYzLjA1ODMgNDguNDM4NiA2My4zNzM1IDQ4LjcwMSA2My42MDIxIDQ5LjA1MjJDNjMuODM0OCA0OS4zOTkzIDYzLjk1MTIgNDkuODM1MSA2My45NTEyIDUwLjM1OTlWNTMuNDEzMUM2My45NTEyIDUzLjcyNjIgNjMuOTcyMyA1NC4wMDc2IDY0LjAxNDYgNTQuMjU3M0M2NC4wNjEyIDU0LjUwMjggNjQuMTI2OCA1NC43MTY1IDY0LjIxMTQgNTQuODk4NFY1NUg2Mi42NDM2QzYyLjU3MTYgNTQuODM1IDYyLjUxNDUgNTQuNjI1NSA2Mi40NzIyIDU0LjM3MTZDNjIuNDM0MSA1NC4xMTM0IDYyLjQxNSA1My44NjM4IDYyLjQxNSA1My42MjI2Wk02Mi42MzcyIDUwLjgyMzJMNjIuNjQ5OSA1MS43NjlINjEuNTUxOEM2MS4yNjgyIDUxLjc2OSA2MS4wMTg2IDUxLjc5NjUgNjAuODAyNyA1MS44NTE2QzYwLjU4NjkgNTEuOTAyMyA2MC40MDcxIDUxLjk3ODUgNjAuMjYzMiA1Mi4wODAxQzYwLjExOTMgNTIuMTgxNiA2MC4wMTE0IDUyLjMwNDQgNTkuOTM5NSA1Mi40NDgyQzU5Ljg2NzUgNTIuNTkyMSA1OS44MzE1IDUyLjc1NSA1OS44MzE1IDUyLjkzN0M1OS44MzE1IDUzLjExOSA1OS44NzM5IDUzLjI4NjEgNTkuOTU4NSA1My40Mzg1QzYwLjA0MzEgNTMuNTg2NiA2MC4xNjU5IDUzLjcwMyA2MC4zMjY3IDUzLjc4NzZDNjAuNDkxNyA1My44NzIyIDYwLjY5MDYgNTMuOTE0NiA2MC45MjMzIDUzLjkxNDZDNjEuMjM2NSA1My45MTQ2IDYxLjUwOTQgNTMuODUxMSA2MS43NDIyIDUzLjcyNDFDNjEuOTc5MiA1My41OTI5IDYyLjE2NTQgNTMuNDM0MiA2Mi4zMDA4IDUzLjI0OEM2Mi40MzYyIDUzLjA1NzYgNjIuNTA4MSA1Mi44Nzc4IDYyLjUxNjYgNTIuNzA4NUw2My4wMTE3IDUzLjM4NzdDNjIuOTYwOSA1My41NjEyIDYyLjg3NDIgNTMuNzQ3NCA2Mi43NTE1IDUzLjk0NjNDNjIuNjI4NyA1NC4xNDUyIDYyLjQ2NzkgNTQuMzM1NiA2Mi4yNjkgNTQuNTE3NkM2Mi4wNzQ0IDU0LjY5NTMgNjEuODM5NSA1NC44NDEzIDYxLjU2NDUgNTQuOTU1NkM2MS4yOTM2IDU1LjA2OTggNjAuOTgwNSA1NS4xMjcgNjAuNjI1IDU1LjEyN0M2MC4xNzY0IDU1LjEyNyA1OS43NzY1IDU1LjAzODEgNTkuNDI1MyA1NC44NjA0QzU5LjA3NDEgNTQuNjc4NCA1OC43OTkgNTQuNDM1MSA1OC42MDAxIDU0LjEzMDRDNTguNDAxMiA1My44MjE1IDU4LjMwMTggNTMuNDcyMyA1OC4zMDE4IDUzLjA4M0M1OC4zMDE4IDUyLjcxOTEgNTguMzY5NSA1Mi4zOTc1IDU4LjUwNDkgNTIuMTE4MkM1OC42NDQ1IDUxLjgzNDYgNTguODQ3NyA1MS41OTc3IDU5LjExNDMgNTEuNDA3MkM1OS4zODUxIDUxLjIxNjggNTkuNzE1MiA1MS4wNzI5IDYwLjEwNDUgNTAuOTc1NkM2MC40OTM4IDUwLjg3NCA2MC45MzgyIDUwLjgyMzIgNjEuNDM3NSA1MC44MjMySDYyLjYzNzJaTTY4Ljk0OTIgNDguMTMxOFY0OS4yNDlINjUuMDc3MVY0OC4xMzE4SDY4Ljk0OTJaTTY2LjE5NDMgNDYuNDQ5N0g2Ny43MjQxVjUzLjEwMjFDNjcuNzI0MSA1My4zMTM2IDY3Ljc1MzcgNTMuNDc2NiA2Ny44MTMgNTMuNTkwOEM2Ny44NzY1IDUzLjcwMDggNjcuOTYzMiA1My43NzQ5IDY4LjA3MzIgNTMuODEzQzY4LjE4MzMgNTMuODUxMSA2OC4zMTIzIDUzLjg3MDEgNjguNDYwNCA1My44NzAxQzY4LjU2NjIgNTMuODcwMSA2OC42Njc4IDUzLjg2MzggNjguNzY1MSA1My44NTExQzY4Ljg2MjUgNTMuODM4NCA2OC45NDA4IDUzLjgyNTcgNjkgNTMuODEzTDY5LjAwNjMgNTQuOTgxQzY4Ljg3OTQgNTUuMDE5IDY4LjczMTMgNTUuMDUyOSA2OC41NjIgNTUuMDgyNUM2OC4zOTcgNTUuMTEyMSA2OC4yMDY1IDU1LjEyNyA2Ny45OTA3IDU1LjEyN0M2Ny42Mzk1IDU1LjEyNyA2Ny4zMjg1IDU1LjA2NTYgNjcuMDU3NiA1NC45NDI5QzY2Ljc4NjggNTQuODE1OSA2Ni41NzUyIDU0LjYxMDcgNjYuNDIyOSA1NC4zMjcxQzY2LjI3MDUgNTQuMDQzNiA2Ni4xOTQzIDUzLjY2NyA2Ni4xOTQzIDUzLjE5NzNWNDYuNDQ5N1pNNzQuNTEyMiA1My4zODEzVjQ4LjEzMThINzYuMDQ4M1Y1NUg3NC42MDExTDc0LjUxMjIgNTMuMzgxM1pNNzQuNzI4IDUxLjk1MzFMNzUuMjQyMiA1MS45NDA0Qzc1LjI0MjIgNTIuNDAxNyA3NS4xOTE0IDUyLjgyNyA3NS4wODk4IDUzLjIxNjNDNzQuOTg4MyA1My42MDE0IDc0LjgzMTcgNTMuOTM3OCA3NC42MjAxIDU0LjIyNTZDNzQuNDA4NSA1NC41MDkxIDc0LjEzNzcgNTQuNzMxMyA3My44MDc2IDU0Ljg5MjFDNzMuNDc3NSA1NS4wNDg3IDczLjA4MTkgNTUuMTI3IDcyLjYyMDYgNTUuMTI3QzcyLjI4NjMgNTUuMTI3IDcxLjk3OTUgNTUuMDc4MyA3MS43MDAyIDU0Ljk4MUM3MS40MjA5IDU0Ljg4MzYgNzEuMTc5NyA1NC43MzM0IDcwLjk3NjYgNTQuNTMwM0M3MC43Nzc3IDU0LjMyNzEgNzAuNjIzMiA1NC4wNjI3IDcwLjUxMzIgNTMuNzM2OEM3MC40MDMyIDUzLjQxMSA3MC4zNDgxIDUzLjAyMTYgNzAuMzQ4MSA1Mi41Njg4VjQ4LjEzMThINzEuODc3OVY1Mi41ODE1QzcxLjg3NzkgNTIuODMxMiA3MS45MDc2IDUzLjA0MDcgNzEuOTY2OCA1My4yMUM3Mi4wMjYgNTMuMzc1IDcyLjEwNjQgNTMuNTA4MyA3Mi4yMDggNTMuNjA5OUM3Mi4zMDk2IDUzLjcxMTQgNzIuNDI4MSA1My43ODM0IDcyLjU2MzUgNTMuODI1N0M3Mi42OTg5IDUzLjg2OCA3Mi44NDI4IDUzLjg4OTIgNzIuOTk1MSA1My44ODkyQzczLjQzMSA1My44ODkyIDczLjc3MzggNTMuODA0NSA3NC4wMjM0IDUzLjYzNTNDNzQuMjc3MyA1My40NjE4IDc0LjQ1NzIgNTMuMjI5IDc0LjU2MyA1Mi45MzdDNzQuNjczIDUyLjY0NSA3NC43MjggNTIuMzE3MSA3NC43MjggNTEuOTUzMVpNNzkuNDAyMyA0OS40Mzk1VjU1SDc3Ljg3MjZWNDguMTMxOEg3OS4zMzI1TDc5LjQwMjMgNDkuNDM5NVpNODEuNTAzNCA0OC4wODc0TDgxLjQ5MDcgNDkuNTA5M0M4MS4zOTc2IDQ5LjQ5MjQgODEuMjk2MSA0OS40Nzk3IDgxLjE4NiA0OS40NzEyQzgxLjA4MDIgNDkuNDYyNyA4MC45NzQ0IDQ5LjQ1ODUgODAuODY4NyA0OS40NTg1QzgwLjYwNjMgNDkuNDU4NSA4MC4zNzU3IDQ5LjQ5NjYgODAuMTc2OCA0OS41NzI4Qzc5Ljk3NzkgNDkuNjQ0NyA3OS44MTA3IDQ5Ljc1MDUgNzkuNjc1MyA0OS44OTAxQzc5LjU0NDEgNTAuMDI1NiA3OS40NDI1IDUwLjE5MDYgNzkuMzcwNiA1MC4zODUzQzc5LjI5ODcgNTAuNTc5OSA3OS4yNTYzIDUwLjc5NzkgNzkuMjQzNyA1MS4wMzkxTDc4Ljg5NDUgNTEuMDY0NUM3OC44OTQ1IDUwLjYzMjggNzguOTM2OCA1MC4yMzI5IDc5LjAyMTUgNDkuODY0N0M3OS4xMDYxIDQ5LjQ5NjYgNzkuMjMzMSA0OS4xNzI5IDc5LjQwMjMgNDguODkzNkM3OS41NzU4IDQ4LjYxNDMgNzkuNzkxNyA0OC4zOTYzIDgwLjA0OTggNDguMjM5N0M4MC4zMTIyIDQ4LjA4MzIgODAuNjE0NyA0OC4wMDQ5IDgwLjk1NzUgNDguMDA0OUM4MS4wNTA2IDQ4LjAwNDkgODEuMTUwMSA0OC4wMTMzIDgxLjI1NTkgNDguMDMwM0M4MS4zNjU5IDQ4LjA0NzIgODEuNDQ4NCA0OC4wNjYyIDgxLjUwMzQgNDguMDg3NFpNODUuNjEyOCA1NS4xMjdDODUuMTA1IDU1LjEyNyA4NC42NDU4IDU1LjA0NDQgODQuMjM1NCA1NC44Nzk0QzgzLjgyOTEgNTQuNzEwMSA4My40ODIxIDU0LjQ3NTMgODMuMTk0MyA1NC4xNzQ4QzgyLjkxMDggNTMuODc0MyA4Mi42OTI5IDUzLjUyMSA4Mi41NDA1IDUzLjExNDdDODIuMzg4MiA1Mi43MDg1IDgyLjMxMiA1Mi4yNzA1IDgyLjMxMiA1MS44MDA4VjUxLjU0NjlDODIuMzEyIDUxLjAwOTQgODIuMzkwMyA1MC41MjI4IDgyLjU0NjkgNTAuMDg2OUM4Mi43MDM1IDQ5LjY1MSA4Mi45MjE0IDQ5LjI3ODYgODMuMjAwNyA0OC45Njk3QzgzLjQ4IDQ4LjY1NjYgODMuODEwMSA0OC40MTc1IDg0LjE5MDkgNDguMjUyNEM4NC41NzE4IDQ4LjA4NzQgODQuOTg0NCA0OC4wMDQ5IDg1LjQyODcgNDguMDA0OUM4NS45MTk2IDQ4LjAwNDkgODYuMzQ5MSA0OC4wODc0IDg2LjcxNzMgNDguMjUyNEM4Ny4wODU0IDQ4LjQxNzUgODcuMzkwMSA0OC42NTAyIDg3LjYzMTMgNDguOTUwN0M4Ny44NzY4IDQ5LjI0NjkgODguMDU4OCA0OS42MDAzIDg4LjE3NzIgNTAuMDEwN0M4OC4zIDUwLjQyMTIgODguMzYxMyA1MC44NzQgODguMzYxMyA1MS4zNjkxVjUyLjAyMjlIODMuMDU0N1Y1MC45MjQ4SDg2Ljg1MDZWNTAuODA0MkM4Ni44NDIxIDUwLjUyOTEgODYuNzg3MSA1MC4yNzEgODYuNjg1NSA1MC4wMjk4Qzg2LjU4ODIgNDkuNzg4NiA4Ni40MzggNDkuNTkzOSA4Ni4yMzQ5IDQ5LjQ0NThDODYuMDMxNyA0OS4yOTc3IDg1Ljc2MDkgNDkuMjIzNiA4NS40MjI0IDQ5LjIyMzZDODUuMTY4NSA0OS4yMjM2IDg0Ljk0MjEgNDkuMjc4NiA4NC43NDMyIDQ5LjM4ODdDODQuNTQ4NSA0OS40OTQ1IDg0LjM4NTYgNDkuNjQ4OSA4NC4yNTQ0IDQ5Ljg1MjFDODQuMTIzMiA1MC4wNTUyIDg0LjAyMTYgNTAuMzAwNiA4My45NDk3IDUwLjU4ODRDODMuODgyIDUwLjg3MTkgODMuODQ4MSA1MS4xOTE0IDgzLjg0ODEgNTEuNTQ2OVY1MS44MDA4QzgzLjg0ODEgNTIuMTAxMiA4My44ODgzIDUyLjM4MDUgODMuOTY4OCA1Mi42Mzg3Qzg0LjA1MzQgNTIuODkyNiA4NC4xNzYxIDUzLjExNDcgODQuMzM2OSA1My4zMDUyQzg0LjQ5NzcgNTMuNDk1NiA4NC42OTI0IDUzLjY0NTggODQuOTIwOSA1My43NTU5Qzg1LjE0OTQgNTMuODYxNyA4NS40MDk3IDUzLjkxNDYgODUuNzAxNyA1My45MTQ2Qzg2LjA2OTggNTMuOTE0NiA4Ni4zOTc4IDUzLjg0MDUgODYuNjg1NSA1My42OTI0Qzg2Ljk3MzMgNTMuNTQ0MyA4Ny4yMjMgNTMuMzM0OCA4Ny40MzQ2IDUzLjA2NEw4OC4yNDA3IDUzLjg0NDdDODguMDkyNiA1NC4wNjA1IDg3LjkwMDEgNTQuMjY3OSA4Ny42NjMxIDU0LjQ2NjhDODcuNDI2MSA1NC42NjE1IDg3LjEzNjIgNTQuODIwMSA4Ni43OTM1IDU0Ljk0MjlDODYuNDU0OSA1NS4wNjU2IDg2LjA2MTQgNTUuMTI3IDg1LjYxMjggNTUuMTI3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjMuMTYzNiAxMDguMDI2VjExMEgxMy4xMzA4VjEwOC4zMDRMMTguMDAzMyAxMDIuOTg5QzE4LjUzNzggMTAyLjM4NiAxOC45NTkzIDEwMS44NjUgMTkuMjY3NyAxMDEuNDI3QzE5LjU3NjEgMTAwLjk4OCAxOS43OTE5IDEwMC41OTQgMTkuOTE1MyAxMDAuMjQ1QzIwLjA0NTUgOTkuODg4MyAyMC4xMTA2IDk5LjU0MjIgMjAuMTEwNiA5OS4yMDY0QzIwLjExMDYgOTguNzMzNiAyMC4wMjE1IDk4LjMxODkgMTkuODQzMyA5Ny45NjI2QzE5LjY3MiA5Ny41OTk0IDE5LjQxODQgOTcuMzE1IDE5LjA4MjYgOTcuMTA5NEMxOC43NDY4IDk2Ljg5NjkgMTguMzM5MSA5Ni43OTA3IDE3Ljg1OTQgOTYuNzkwN0MxNy4zMDQzIDk2Ljc5MDcgMTYuODM4MyA5Ni45MTA2IDE2LjQ2MTMgOTcuMTUwNUMxNi4wODQ0IDk3LjM5MDQgMTUuOCA5Ny43MjI3IDE1LjYwODEgOTguMTQ3NkMxNS40MTYzIDk4LjU2NTcgMTUuMzIwMyA5OS4wNDU0IDE1LjMyMDMgOTkuNTg2OEgxMi44NDI5QzEyLjg0MjkgOTguNzE2NCAxMy4wNDE3IDk3LjkyMTUgMTMuNDM5MSA5Ny4yMDE5QzEzLjgzNjYgOTYuNDc1NSAxNC40MTIzIDk1Ljg5OTggMTUuMTY2MSA5NS40NzQ5QzE1LjkyIDk1LjA0MzIgMTYuODI4IDk0LjgyNzMgMTcuODkwMiA5NC44MjczQzE4Ljg5MDggOTQuODI3MyAxOS43NDA1IDk0Ljk5NTIgMjAuNDM5NiA5NS4zMzFDMjEuMTM4NiA5NS42NjY4IDIxLjY2OTcgOTYuMTQzMSAyMi4wMzI5IDk2Ljc1OTlDMjIuNDAzIDk3LjM3NjcgMjIuNTg4IDk4LjEwNjUgMjIuNTg4IDk4Ljk0OTRDMjIuNTg4IDk5LjQxNTQgMjIuNTEyNiA5OS44NzggMjIuMzYxOCAxMDAuMzM3QzIyLjIxMTEgMTAwLjc5NiAyMS45OTUyIDEwMS4yNTUgMjEuNzE0MiAxMDEuNzE1QzIxLjQ0MDEgMTAyLjE2NyAyMS4xMTQ2IDEwMi42MjMgMjAuNzM3NyAxMDMuMDgyQzIwLjM2MDcgMTAzLjUzNCAxOS45NDYxIDEwMy45OTMgMTkuNDkzOCAxMDQuNDU5TDE2LjI1NTggMTA4LjAyNkgyMy4xNjM2Wk0zNS4xMjkxIDEwOC4wMjZWMTEwSDI1LjA5NjJWMTA4LjMwNEwyOS45Njg3IDEwMi45ODlDMzAuNTAzMyAxMDIuMzg2IDMwLjkyNDcgMTAxLjg2NSAzMS4yMzMxIDEwMS40MjdDMzEuNTQxNSAxMDAuOTg4IDMxLjc1NzQgMTAwLjU5NCAzMS44ODA3IDEwMC4yNDVDMzIuMDExIDk5Ljg4ODMgMzIuMDc2MSA5OS41NDIyIDMyLjA3NjEgOTkuMjA2NEMzMi4wNzYxIDk4LjczMzYgMzEuOTg3IDk4LjMxODkgMzEuODA4OCA5Ny45NjI2QzMxLjYzNzUgOTcuNTk5NCAzMS4zODM5IDk3LjMxNSAzMS4wNDgxIDk3LjEwOTRDMzAuNzEyMyA5Ni44OTY5IDMwLjMwNDUgOTYuNzkwNyAyOS44MjQ4IDk2Ljc5MDdDMjkuMjY5NyA5Ni43OTA3IDI4LjgwMzcgOTYuOTEwNiAyOC40MjY4IDk3LjE1MDVDMjguMDQ5OSA5Ny4zOTA0IDI3Ljc2NTUgOTcuNzIyNyAyNy41NzM2IDk4LjE0NzZDMjcuMzgxNyA5OC41NjU3IDI3LjI4NTggOTkuMDQ1NCAyNy4yODU4IDk5LjU4NjhIMjQuODA4NEMyNC44MDg0IDk4LjcxNjQgMjUuMDA3MSA5Ny45MjE1IDI1LjQwNDYgOTcuMjAxOUMyNS44MDIxIDk2LjQ3NTUgMjYuMzc3NyA5NS44OTk4IDI3LjEzMTYgOTUuNDc0OUMyNy44ODU0IDk1LjA0MzIgMjguNzkzNCA5NC44MjczIDI5Ljg1NTcgOTQuODI3M0MzMC44NTYyIDk0LjgyNzMgMzEuNzA2IDk0Ljk5NTIgMzIuNDA1IDk1LjMzMUMzMy4xMDQgOTUuNjY2OCAzMy42MzUxIDk2LjE0MzEgMzMuOTk4MyA5Ni43NTk5QzM0LjM2ODQgOTcuMzc2NyAzNC41NTM0IDk4LjEwNjUgMzQuNTUzNCA5OC45NDk0QzM0LjU1MzQgOTkuNDE1NCAzNC40NzgxIDk5Ljg3OCAzNC4zMjczIDEwMC4zMzdDMzQuMTc2NSAxMDAuNzk2IDMzLjk2MDYgMTAxLjI1NSAzMy42Nzk3IDEwMS43MTVDMzMuNDA1NiAxMDIuMTY3IDMzLjA4IDEwMi42MjMgMzIuNzAzMSAxMDMuMDgyQzMyLjMyNjIgMTAzLjUzNCAzMS45MTE2IDEwMy45OTMgMzEuNDU5MyAxMDQuNDU5TDI4LjIyMTIgMTA4LjAyNkgzNS4xMjkxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuMzI2MSA5Ny41NzJDNDEuMzI2MSA5Ny4wNzE3IDQxLjQ0OTQgOTYuNjEyNSA0MS42OTYxIDk2LjE5NDVDNDEuOTQyOCA5NS43NzY1IDQyLjI3MTggOTUuNDQ0MSA0Mi42ODMgOTUuMTk3NEM0My4xMDEgOTQuOTQzOCA0My41NTMzIDk0LjgxNyA0NC4wMzk5IDk0LjgxN0M0NC41MzMzIDk0LjgxNyA0NC45ODIyIDk0Ljk0MzggNDUuMzg2NSA5NS4xOTc0QzQ1Ljc5MDggOTUuNDQ0MSA0Ni4xMTI5IDk1Ljc3NjUgNDYuMzUyOCA5Ni4xOTQ1QzQ2LjU5OTUgOTYuNjEyNSA0Ni43MjI5IDk3LjA3MTcgNDYuNzIyOSA5Ny41NzJDNDYuNzIyOSA5OC4wNzIyIDQ2LjU5OTUgOTguNTMxNCA0Ni4zNTI4IDk4Ljk0OTRDNDYuMTEyOSA5OS4zNjA2IDQ1Ljc5MDggOTkuNjg2MSA0NS4zODY1IDk5LjkyNkM0NC45ODIyIDEwMC4xNjYgNDQuNTMzMyAxMDAuMjg2IDQ0LjAzOTkgMTAwLjI4NkM0My41NTMzIDEwMC4yODYgNDMuMTAxIDEwMC4xNjYgNDIuNjgzIDk5LjkyNkM0Mi4yNzE4IDk5LjY4NjEgNDEuOTQyOCA5OS4zNjA2IDQxLjY5NjEgOTguOTQ5NEM0MS40NDk0IDk4LjUzMTQgNDEuMzI2MSA5OC4wNzIyIDQxLjMyNjEgOTcuNTcyWk00Mi43MTM4IDk3LjU3MkM0Mi43MTM4IDk3Ljk0MiA0Mi44NDQgOTguMjUzOCA0My4xMDQ0IDk4LjUwNzRDNDMuMzY0OSA5OC43NTQxIDQzLjY3NjcgOTguODc3NSA0NC4wMzk5IDk4Ljg3NzVDNDQuNDAzMSA5OC44Nzc1IDQ0LjcwODEgOTguNzU0MSA0NC45NTQ4IDk4LjUwNzRDNDUuMjAxNSA5OC4yNjA3IDQ1LjMyNDggOTcuOTQ4OSA0NS4zMjQ4IDk3LjU3MkM0NS4zMjQ4IDk3LjE4ODIgNDUuMjAxNSA5Ni44Njk1IDQ0Ljk1NDggOTYuNjE2QzQ0LjcwODEgOTYuMzYyNCA0NC40MDMxIDk2LjIzNTYgNDQuMDM5OSA5Ni4yMzU2QzQzLjY3NjcgOTYuMjM1NiA0My4zNjQ5IDk2LjM2MjQgNDMuMTA0NCA5Ni42MTZDNDIuODQ0IDk2Ljg2OTUgNDIuNzEzOCA5Ny4xODgyIDQyLjcxMzggOTcuNTcyWk01OC40MjEgMTA1LjEyN0g2MC45OTA5QzYwLjkwODcgMTA2LjEwNyA2MC42MzQ2IDEwNi45ODEgNjAuMTY4NiAxMDcuNzQ5QzU5LjcwMjYgMTA4LjUwOSA1OS4wNDgxIDEwOS4xMDkgNTguMjA1MiAxMDkuNTQ4QzU3LjM2MjIgMTA5Ljk4NiA1Ni4zMzc3IDExMC4yMDYgNTUuMTMxNiAxMTAuMjA2QzU0LjIwNjQgMTEwLjIwNiA1My4zNzM4IDExMC4wNDEgNTIuNjMzNiAxMDkuNzEyQzUxLjg5MzUgMTA5LjM3NiA1MS4yNTk2IDEwOC45MDQgNTAuNzMxOSAxMDguMjk0QzUwLjIwNDIgMTA3LjY3NyA0OS43OTk5IDEwNi45MzMgNDkuNTE4OSAxMDYuMDYzQzQ5LjI0NDggMTA1LjE5MyA0OS4xMDc3IDEwNC4yMTkgNDkuMTA3NyAxMDMuMTQ0VjEwMS45QzQ5LjEwNzcgMTAwLjgyNCA0OS4yNDgyIDk5Ljg1MDYgNDkuNTI5MiA5OC45ODAzQzQ5LjgxNyA5OC4xMDk5IDUwLjIyODIgOTcuMzY2NCA1MC43NjI3IDk2Ljc0OTZDNTEuMjk3MyA5Ni4xMjYgNTEuOTM4IDk1LjY0OTcgNTIuNjg1IDk1LjMyMDdDNTMuNDM4OSA5NC45OTE4IDU0LjI4NTIgOTQuODI3MyA1NS4yMjQxIDk0LjgyNzNDNTYuNDE2NSA5NC44MjczIDU3LjQyMzkgOTUuMDQ2NiA1OC4yNDYzIDk1LjQ4NTJDNTkuMDY4NyA5NS45MjM4IDU5LjcwNiA5Ni41MzAzIDYwLjE1ODMgOTcuMzA0N0M2MC42MTc0IDk4LjA3OTEgNjAuODk4NCA5OC45NjY2IDYxLjAwMTIgOTkuOTY3MUg1OC40MzEzQzU4LjM2MjggOTkuMzIyOSA1OC4yMTIgOTguNzcxMyA1Ny45NzkgOTguMzEyMUM1Ny43NTI5IDk3Ljg1MjkgNTcuNDE3MSA5Ny41MDM0IDU2Ljk3MTYgOTcuMjYzNkM1Ni41MjYyIDk3LjAxNjkgNTUuOTQzNyA5Ni44OTM1IDU1LjIyNDEgOTYuODkzNUM1NC42MzQ3IDk2Ljg5MzUgNTQuMTIwNyA5Ny4wMDMyIDUzLjY4MjEgOTcuMjIyNUM1My4yNDM1IDk3LjQ0MTggNTIuODc2OSA5Ny43NjM5IDUyLjU4MjIgOTguMTg4N0M1Mi4yODc1IDk4LjYxMzYgNTIuMDY0OCA5OS4xMzc5IDUxLjkxNDEgOTkuNzYxNUM1MS43NzAxIDEwMC4zNzggNTEuNjk4MiAxMDEuMDg0IDUxLjY5ODIgMTAxLjg3OVYxMDMuMTQ0QzUxLjY5ODIgMTAzLjg5NyA1MS43NjMzIDEwNC41ODMgNTEuODkzNSAxMDUuMTk5QzUyLjAzMDYgMTA1LjgwOSA1Mi4yMzYxIDEwNi4zMzQgNTIuNTEwMyAxMDYuNzcyQzUyLjc5MTIgMTA3LjIxMSA1My4xNDc2IDEwNy41NSA1My41NzkzIDEwNy43OUM1NC4wMTExIDEwOC4wMyA1NC41Mjg1IDEwOC4xNSA1NS4xMzE2IDEwOC4xNUM1NS44NjQ4IDEwOC4xNSA1Ni40NTc2IDEwOC4wMzMgNTYuOTA5OSAxMDcuOEM1Ny4zNjkxIDEwNy41NjcgNTcuNzE1MiAxMDcuMjI4IDU3Ljk0ODIgMTA2Ljc4MkM1OC4xODggMTA2LjMzIDU4LjM0NTcgMTA1Ljc3OSA1OC40MjEgMTA1LjEyN1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY4Ljc4MiAxMTEuMTQ4TDc5LjA3MzIgMTA0Ljg3NkM4MC43NjQ3IDEwMy44NDUgODIuMDcgMTAyLjI4NiA4Mi43ODc1IDEwMC40NEw4NS44OTA4IDkyLjQ1NDVDODguMjQ5NSA4Ni4zODQ5IDk2LjgzNzkgODYuMzg0OSA5OS4xOTY3IDkyLjQ1NDVWOTIuNDU0NUMxMDEuMjczIDk3Ljc5NzkgMTA4LjQ4MyA5OC42MzY4IDExMS43MzEgOTMuOTEzTDExNS40MDMgODguNTcyNUMxMTcuNjY3IDg1LjI3OTIgMTIyLjMyOCA4NC43NjUgMTI1LjI1NiA4Ny40ODU2Vjg3LjQ4NTZDMTI4LjUzMyA5MC41MzA0IDEzMy44MyA4OS40NjY1IDEzNS42NzcgODUuMzkyN0wxMzguNTAxIDc5LjE2MzFDMTQxLjYxMiA3Mi4zMDIxIDE1MS4yNjQgNzIuMDQ5MSAxNTQuNzMgNzguNzM3OUwxNjAuMTcgODkuMjM3QzE2Mi4yNjEgOTMuMjcyOCAxNjcuMDI4IDk1LjEyMjkgMTcxLjI5NCA5My41NTQ2TDE3NC42MTIgOTIuMzM0OEMxNzYuODM4IDkxLjUxNjYgMTc5LjI5NyA5MS42MDc1IDE4MS40NTYgOTIuNTg4MUwxODkuNjI3IDk2LjI5ODgiIHN0cm9rZT0iIzNGNTJERCIgc3Ryb2tlLXdpZHRoPSIxLjUwMzc2IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+Cg==", "description": "Displays a single entity historical telemetry values as a simplified chart. Optionally may display the corresponding latest telemetry value.", "descriptor": { "type": "timeseries", @@ -22,6 +22,5 @@ "basicModeDirective": "tb-value-chart-card-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Simple Value and chart card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" }, - "externalId": null, "tags": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/single_switch.json b/application/src/main/data/json/system/widget_types/single_switch.json index b14cf633a2..8faea8ba93 100644 --- a/application/src/main/data/json/system/widget_types/single_switch.json +++ b/application/src/main/data/json/system/widget_types/single_switch.json @@ -3,7 +3,7 @@ "name": "Single Switch", "deprecated": false, "image": "tb-image:c2luZ2xlLXN3aXRjaC5zdmc=:IlNpbmdsZSBTd2l0Y2giIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iMzEuNTc3MSIgeT0iNjUuMDc0MiIgd2lkdGg9IjU2Ljk5MDEiIGhlaWdodD0iMjkuODUyIiByeD0iMTQuOTI2IiBmaWxsPSIjNTQ2OUZGIi8+CjxjaXJjbGUgY3g9IjczLjY0MDkiIGN5PSI4MC4wMDAzIiByPSIxMi4yMTIyIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTA5LjI0MSA4My40NzE3QzEwOS4yNDEgODMuMTQ5NCAxMDkuMTkxIDgyLjg2MyAxMDkuMDkxIDgyLjYxMjNDMTA4Ljk5OCA4Mi4zNjE3IDEwOC44MjkgODIuMTMyNSAxMDguNTg2IDgxLjkyNDhDMTA4LjM0MiA4MS43MTcxIDEwNy45OTkgODEuNTE2NiAxMDcuNTU1IDgxLjMyMzJDMTA3LjExOCA4MS4xMjI3IDEwNi41NTkgODAuOTE4NiAxMDUuODc5IDgwLjcxMDlDMTA1LjEzNCA4MC40ODE4IDEwNC40NDcgODAuMjI3NSAxMDMuODE2IDc5Ljk0ODJDMTAzLjE5MyA3OS42NjE4IDEwMi42NDkgNzkuMzMyNCAxMDIuMTg0IDc4Ljk2QzEwMS43MTggNzguNTgwNCAxMDEuMzU2IDc4LjE0NzEgMTAxLjA5OSA3Ny42NjAyQzEwMC44NDEgNzcuMTY2IDEwMC43MTIgNzYuNTk2NyAxMDAuNzEyIDc1Ljk1MjFDMTAwLjcxMiA3NS4zMTQ4IDEwMC44NDQgNzQuNzM0NyAxMDEuMTA5IDc0LjIxMTlDMTAxLjM4MiA3My42ODkxIDEwMS43NjUgNzMuMjM4IDEwMi4yNTkgNzIuODU4NEMxMDIuNzYgNzIuNDcxNyAxMDMuMzUxIDcyLjE3NDUgMTA0LjAzMSA3MS45NjY4QzEwNC43MTIgNzEuNzUyIDEwNS40NjQgNzEuNjQ0NSAxMDYuMjg3IDcxLjY0NDVDMTA3LjQ0NyA3MS42NDQ1IDEwOC40NDYgNzEuODU5NCAxMDkuMjg0IDcyLjI4OTFDMTEwLjEyOSA3Mi43MTg4IDExMC43NzcgNzMuMjk1MiAxMTEuMjI5IDc0LjAxODZDMTExLjY4NyA3NC43NDE5IDExMS45MTYgNzUuNTQwNCAxMTEuOTE2IDc2LjQxNDFIMTA5LjI0MUMxMDkuMjQxIDc1Ljg5ODQgMTA5LjEzIDc1LjQ0MzcgMTA4LjkwOCA3NS4wNDk4QzEwOC42OTMgNzQuNjQ4OCAxMDguMzY0IDc0LjMzMzcgMTA3LjkyIDc0LjEwNDVDMTA3LjQ4MyA3My44NzUzIDEwNi45MjggNzMuNzYwNyAxMDYuMjU1IDczLjc2MDdDMTA1LjYxOCA3My43NjA3IDEwNS4wODggNzMuODU3NCAxMDQuNjY1IDc0LjA1MDhDMTA0LjI0MyA3NC4yNDQxIDEwMy45MjcgNzQuNTA1NSAxMDMuNzIgNzQuODM1QzEwMy41MTIgNzUuMTY0NCAxMDMuNDA4IDc1LjUzNjggMTAzLjQwOCA3NS45NTIxQzEwMy40MDggNzYuMjQ1OCAxMDMuNDc2IDc2LjUxNDMgMTAzLjYxMiA3Ni43NTc4QzEwMy43NDggNzYuOTk0MSAxMDMuOTU2IDc3LjIxNjEgMTA0LjIzNSA3Ny40MjM4QzEwNC41MTUgNzcuNjI0MyAxMDQuODY2IDc3LjgxNDEgMTA1LjI4OCA3Ny45OTMyQzEwNS43MTEgNzguMTcyMiAxMDYuMjA4IDc4LjM0NDEgMTA2Ljc4MSA3OC41MDg4QzEwNy42NDggNzguNzY2NiAxMDguNDAzIDc5LjA1MzEgMTA5LjA0OCA3OS4zNjgyQzEwOS42OTIgNzkuNjc2MSAxMTAuMjI5IDgwLjAyNyAxMTAuNjU5IDgwLjQyMDlDMTExLjA4OSA4MC44MTQ4IDExMS40MTEgODEuMjYyNCAxMTEuNjI2IDgxLjc2MzdDMTExLjg0MSA4Mi4yNTc4IDExMS45NDggODIuODIgMTExLjk0OCA4My40NTAyQzExMS45NDggODQuMTA5IDExMS44MTYgODQuNzAzNSAxMTEuNTUxIDg1LjIzMzRDMTExLjI4NiA4NS43NTYyIDExMC45MDYgODYuMjAzOCAxMTAuNDEyIDg2LjU3NjJDMTA5LjkyNSA4Ni45NDE0IDEwOS4zMzggODcuMjI0MyAxMDguNjUgODcuNDI0OEMxMDcuOTcgODcuNjE4MiAxMDcuMjExIDg3LjcxNDggMTA2LjM3MyA4Ny43MTQ4QzEwNS42MjEgODcuNzE0OCAxMDQuODggODcuNjE0NiAxMDQuMTQ5IDg3LjQxNDFDMTAzLjQyNiA4Ny4yMTM1IDEwMi43NjcgODYuOTA5MiAxMDIuMTczIDg2LjUwMUMxMDEuNTc4IDg2LjA4NTYgMTAxLjEwNiA4NS41NyAxMDAuNzU1IDg0Ljk1NDFDMTAwLjQwNCA4NC4zMzExIDEwMC4yMjkgODMuNjA0MiAxMDAuMjI5IDgyLjc3MzRIMTAyLjkyNUMxMDIuOTI1IDgzLjI4MTkgMTAzLjAxMSA4My43MTUyIDEwMy4xODMgODQuMDczMkMxMDMuMzYyIDg0LjQzMTMgMTAzLjYwOSA4NC43MjQ5IDEwMy45MjQgODQuOTU0MUMxMDQuMjM5IDg1LjE3NjEgMTA0LjYwNCA4NS4zNDA4IDEwNS4wMiA4NS40NDgyQzEwNS40NDIgODUuNTU1NyAxMDUuODkzIDg1LjYwOTQgMTA2LjM3MyA4NS42MDk0QzEwNy4wMDMgODUuNjA5NCAxMDcuNTMgODUuNTE5OSAxMDcuOTUyIDg1LjM0MDhDMTA4LjM4MiA4NS4xNjE4IDEwOC43MDQgODQuOTExMSAxMDguOTE5IDg0LjU4ODlDMTA5LjEzNCA4NC4yNjY2IDEwOS4yNDEgODMuODk0MiAxMDkuMjQxIDgzLjQ3MTdaTTExNy41NzcgODQuOTIxOUwxMjAuMjYzIDc1Ljg3N0gxMjEuOTE3TDEyMS40NjYgNzguNTg0TDExOC43NTkgODcuNUgxMTcuMjc2TDExNy41NzcgODQuOTIxOVpNMTE1Ljk5OCA3NS44NzdMMTE4LjA5MyA4NC45NjQ4TDExOC4yNjUgODcuNUgxMTYuNjFMMTEzLjQ2MyA3NS44NzdIMTE1Ljk5OFpNMTI0LjQzMSA4NC44NTc0TDEyNi40NjEgNzUuODc3SDEyOC45ODVMMTI1Ljg0OSA4Ny41SDEyNC4xOTRMMTI0LjQzMSA4NC44NTc0Wk0xMjIuMTk2IDc1Ljg3N0wxMjQuODUgODQuODE0NUwxMjUuMTgzIDg3LjVIMTIzLjdMMTIwLjk2MSA3OC41NzMyTDEyMC41MSA3NS44NzdIMTIyLjE5NlpNMTMzLjg2MiA3NS44NzdWODcuNUgxMzEuMjYzVjc1Ljg3N0gxMzMuODYyWk0xMzEuMDkxIDcyLjgyNjJDMTMxLjA5MSA3Mi40MzIzIDEzMS4yMiA3Mi4xMDY0IDEzMS40NzggNzEuODQ4NkMxMzEuNzQzIDcxLjU4MzcgMTMyLjEwOCA3MS40NTEyIDEzMi41NzMgNzEuNDUxMkMxMzMuMDMyIDcxLjQ1MTIgMTMzLjM5MyA3MS41ODM3IDEzMy42NTggNzEuODQ4NkMxMzMuOTIzIDcyLjEwNjQgMTM0LjA1NiA3Mi40MzIzIDEzNC4wNTYgNzIuODI2MkMxMzQuMDU2IDczLjIxMjkgMTMzLjkyMyA3My41MzUyIDEzMy42NTggNzMuNzkzQzEzMy4zOTMgNzQuMDUwOCAxMzMuMDMyIDc0LjE3OTcgMTMyLjU3MyA3NC4xNzk3QzEzMi4xMDggNzQuMTc5NyAxMzEuNzQzIDc0LjA1MDggMTMxLjQ3OCA3My43OTNDMTMxLjIyIDczLjUzNTIgMTMxLjA5MSA3My4yMTI5IDEzMS4wOTEgNzIuODI2MlpNMTQyLjM3IDc1Ljg3N1Y3Ny43Njc2SDEzNS44MTdWNzUuODc3SDE0Mi4zN1pNMTM3LjcwOCA3My4wMzAzSDE0MC4yOTdWODQuMjg4MUMxNDAuMjk3IDg0LjY0NjIgMTQwLjM0NyA4NC45MjE5IDE0MC40NDcgODUuMTE1MkMxNDAuNTU1IDg1LjMwMTQgMTQwLjcwMSA4NS40MjY4IDE0MC44ODggODUuNDkxMkMxNDEuMDc0IDg1LjU1NTcgMTQxLjI5MiA4NS41ODc5IDE0MS41NDMgODUuNTg3OUMxNDEuNzIyIDg1LjU4NzkgMTQxLjg5NCA4NS41NzcxIDE0Mi4wNTkgODUuNTU1N0MxNDIuMjIzIDg1LjUzNDIgMTQyLjM1NiA4NS41MTI3IDE0Mi40NTYgODUuNDkxMkwxNDIuNDY3IDg3LjQ2NzhDMTQyLjI1MiA4Ny41MzIyIDE0Mi4wMDEgODcuNTg5NSAxNDEuNzE1IDg3LjYzOTZDMTQxLjQzNiA4Ny42ODk4IDE0MS4xMTMgODcuNzE0OCAxNDAuNzQ4IDg3LjcxNDhDMTQwLjE1NCA4Ny43MTQ4IDEzOS42MjcgODcuNjExIDEzOS4xNjkgODcuNDAzM0MxMzguNzExIDg3LjE4ODUgMTM4LjM1MyA4Ni44NDExIDEzOC4wOTUgODYuMzYxM0MxMzcuODM3IDg1Ljg4MTUgMTM3LjcwOCA4NS4yNDQxIDEzNy43MDggODQuNDQ5MlY3My4wMzAzWk0xNDkuNDYgODUuNjUyM0MxNDkuODgyIDg1LjY1MjMgMTUwLjI2MiA4NS41NyAxNTAuNTk5IDg1LjQwNTNDMTUwLjk0MiA4NS4yMzM0IDE1MS4yMTggODQuOTk3MSAxNTEuNDI2IDg0LjY5NjNDMTUxLjY0MSA4NC4zOTU1IDE1MS43NTkgODQuMDQ4MiAxNTEuNzggODMuNjU0M0gxNTQuMjE5QzE1NC4yMDQgODQuNDA2MiAxNTMuOTgyIDg1LjA5MDIgMTUzLjU1MyA4NS43MDYxQzE1My4xMjMgODYuMzIxOSAxNTIuNTU0IDg2LjgxMjUgMTUxLjg0NSA4Ny4xNzc3QzE1MS4xMzYgODcuNTM1OCAxNTAuMzUyIDg3LjcxNDggMTQ5LjQ5MiA4Ny43MTQ4QzE0OC42MDQgODcuNzE0OCAxNDcuODMxIDg3LjU2NDUgMTQ3LjE3MiA4Ny4yNjM3QzE0Ni41MTMgODYuOTU1NyAxNDUuOTY1IDg2LjUzMzIgMTQ1LjUyOCA4NS45OTYxQzE0NS4wOTEgODUuNDU5IDE0NC43NjIgODQuODM5NSAxNDQuNTQgODQuMTM3N0MxNDQuMzI1IDgzLjQzNTkgMTQ0LjIxOCA4Mi42ODM5IDE0NC4yMTggODEuODgxOFY4MS41MDU5QzE0NC4yMTggODAuNzAzOCAxNDQuMzI1IDc5Ljk1MTggMTQ0LjU0IDc5LjI1QzE0NC43NjIgNzguNTQxIDE0NS4wOTEgNzcuOTE4IDE0NS41MjggNzcuMzgwOUMxNDUuOTY1IDc2Ljg0MzggMTQ2LjUxMyA3Ni40MjQ4IDE0Ny4xNzIgNzYuMTI0QzE0Ny44MzEgNzUuODE2MSAxNDguNjAxIDc1LjY2MjEgMTQ5LjQ4MSA3NS42NjIxQzE1MC40MTIgNzUuNjYyMSAxNTEuMjI5IDc1Ljg0ODMgMTUxLjkzMSA3Ni4yMjA3QzE1Mi42MzIgNzYuNTg1OSAxNTMuMTg0IDc3LjA5OCAxNTMuNTg1IDc3Ljc1NjhDMTUzLjk5MyA3OC40MDg1IDE1NC4yMDQgNzkuMTY3NiAxNTQuMjE5IDgwLjAzNDJIMTUxLjc4QzE1MS43NTkgNzkuNjA0NSAxNTEuNjUxIDc5LjIxNzggMTUxLjQ1OCA3OC44NzRDMTUxLjI3MiA3OC41MjMxIDE1MS4wMDcgNzguMjQzOCAxNTAuNjYzIDc4LjAzNjFDMTUwLjMyNiA3Ny44Mjg1IDE0OS45MjIgNzcuNzI0NiAxNDkuNDQ5IDc3LjcyNDZDMTQ4LjkyNiA3Ny43MjQ2IDE0OC40OTMgNzcuODMyIDE0OC4xNDkgNzguMDQ2OUMxNDcuODA2IDc4LjI1NDYgMTQ3LjUzNyA3OC41NDEgMTQ3LjM0NCA3OC45MDYyQzE0Ny4xNSA3OS4yNjQzIDE0Ny4wMTEgNzkuNjY4OSAxNDYuOTI1IDgwLjEyMDFDMTQ2Ljg0NiA4MC41NjQxIDE0Ni44MDcgODEuMDI2IDE0Ni44MDcgODEuNTA1OVY4MS44ODE4QzE0Ni44MDcgODIuMzYxNyAxNDYuODQ2IDgyLjgyNzEgMTQ2LjkyNSA4My4yNzgzQzE0Ny4wMDQgODMuNzI5NSAxNDcuMTQgODQuMTM0MSAxNDcuMzMzIDg0LjQ5MjJDMTQ3LjUzNCA4NC44NDMxIDE0Ny44MDYgODUuMTI2IDE0OC4xNDkgODUuMzQwOEMxNDguNDkzIDg1LjU0ODUgMTQ4LjkzIDg1LjY1MjMgMTQ5LjQ2IDg1LjY1MjNaTTE1OS4xMDYgNzFWODcuNUgxNTYuNTI4VjcxSDE1OS4xMDZaTTE1OC42NTUgODEuMjU4OEwxNTcuODE3IDgxLjI0OEMxNTcuODI1IDgwLjQ0NiAxNTcuOTM2IDc5LjcwNDggMTU4LjE1IDc5LjAyNDRDMTU4LjM3MiA3OC4zNDQxIDE1OC42OCA3Ny43NTMzIDE1OS4wNzQgNzcuMjUyQzE1OS40NzUgNzYuNzQzNSAxNTkuOTU1IDc2LjM1MzIgMTYwLjUxNCA3Ni4wODExQzE2MS4wNzIgNzUuODAxOCAxNjEuNjkyIDc1LjY2MjEgMTYyLjM3MiA3NS42NjIxQzE2Mi45NDUgNzUuNjYyMSAxNjMuNDYxIDc1Ljc0MDkgMTYzLjkxOSA3NS44OTg0QzE2NC4zODQgNzYuMDU2IDE2NC43ODUgNzYuMzEwMiAxNjUuMTIyIDc2LjY2MTFDMTY1LjQ1OSA3Ny4wMDQ5IDE2NS43MTMgNzcuNDU2MSAxNjUuODg1IDc4LjAxNDZDMTY2LjA2NCA3OC41NjYxIDE2Ni4xNTMgNzkuMjM5MyAxNjYuMTUzIDgwLjAzNDJWODcuNUgxNjMuNTU0VjgwLjAxMjdDMTYzLjU1NCA3OS40NTQxIDE2My40NzEgNzkuMDEwMSAxNjMuMzA3IDc4LjY4MDdDMTYzLjE0OSA3OC4zNTEyIDE2Mi45MTYgNzguMTE0OSAxNjIuNjA4IDc3Ljk3MTdDMTYyLjMgNzcuODIxMyAxNjEuOTI0IDc3Ljc0NjEgMTYxLjQ4IDc3Ljc0NjFDMTYxLjAxNSA3Ny43NDYxIDE2MC42MDMgNzcuODM5MiAxNjAuMjQ1IDc4LjAyNTRDMTU5Ljg5NCA3OC4yMTE2IDE1OS42MDEgNzguNDY1OCAxNTkuMzY0IDc4Ljc4ODFDMTU5LjEyOCA3OS4xMTA0IDE1OC45NDkgNzkuNDgyNyAxNTguODI3IDc5LjkwNTNDMTU4LjcxMyA4MC4zMjc4IDE1OC42NTUgODAuNzc5IDE1OC42NTUgODEuMjU4OFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==", - "description": "Sends the command to the device or updates attribute/time-series when the user toggles the slider. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when turn on/off states.", + "description": "Allows users to toggle a slider to send commands to devices or update attributes/time-series data. Configurable settings let users define how to retrieve the initial state and specify actions for the on/off toggle.", "descriptor": { "type": "rpc", "sizeX": 3.5, @@ -30,6 +30,9 @@ "interface", "subroutine call", "inter-process communication", - "server request" + "server request", + "update attribute", + "set attribute", + "add time-series" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/slider.json b/application/src/main/data/json/system/widget_types/slider.json new file mode 100644 index 0000000000..8cbf5326c3 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/slider.json @@ -0,0 +1,39 @@ +{ + "fqn": "slider", + "name": "Slider", + "deprecated": false, + "image": "tb-image:SG9yaXpvbnRhbCBzbGlkZXIuc3Zn:IlNsaWRlciIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik05MC41MDY0IDYwLjkxOFY2My4xNjhINzguMjAxN0w3OC4xMDggNjEuNDY4OEw4NS40NjczIDQ5LjkzNzVIODcuNzI5MUw4NS4yNzk4IDU0LjEzMjhMODEuMDQ5NCA2MC45MThIOTAuNTA2NFpNODguMzczNiA0OS45Mzc1VjY3SDg1LjU0OTRWNDkuOTM3NUg4OC4zNzM2Wk0xMDMuNjA5IDYyLjM0NzdDMTAzLjYwOSA2My40MTAyIDEwMy4zNjMgNjQuMzA0NyAxMDIuODcxIDY1LjAzMTJDMTAyLjM3OSA2NS43NTc4IDEwMS43MDcgNjYuMzA4NiAxMDAuODU1IDY2LjY4MzZDMTAwLjAxMiA2Ny4wNTA4IDk5LjA1ODYgNjcuMjM0NCA5Ny45OTYxIDY3LjIzNDRDOTYuOTMzNiA2Ny4yMzQ0IDk1Ljk3NjYgNjcuMDUwOCA5NS4xMjUgNjYuNjgzNkM5NC4yNzM0IDY2LjMwODYgOTMuNjAxNiA2NS43NTc4IDkzLjEwOTQgNjUuMDMxMkM5Mi42MTcyIDY0LjMwNDcgOTIuMzcxMSA2My40MTAyIDkyLjM3MTEgNjIuMzQ3N0M5Mi4zNzExIDYxLjY0NDUgOTIuNTA3OCA2MS4wMDc4IDkyLjc4MTMgNjAuNDM3NUM5My4wNTQ3IDU5Ljg1OTQgOTMuNDQxNCA1OS4zNjMzIDkzLjk0MTQgNTguOTQ5MkM5NC40NDkyIDU4LjUyNzMgOTUuMDQzIDU4LjIwMzEgOTUuNzIyNyA1Ny45NzY2Qzk2LjQxMDIgNTcuNzUgOTcuMTYwMiA1Ny42MzY3IDk3Ljk3MjcgNTcuNjM2N0M5OS4wNTA4IDU3LjYzNjcgMTAwLjAxNiA1Ny44MzU5IDEwMC44NjcgNTguMjM0NEMxMDEuNzE5IDU4LjYzMjggMTAyLjM4NyA1OS4xODM2IDEwMi44NzEgNTkuODg2N0MxMDMuMzYzIDYwLjU4OTggMTAzLjYwOSA2MS40MTAyIDEwMy42MDkgNjIuMzQ3N1pNMTAwLjc3MyA2Mi4yMDdDMTAwLjc3MyA2MS42MzY3IDEwMC42NTYgNjEuMTM2NyAxMDAuNDIyIDYwLjcwN0MxMDAuMTg4IDYwLjI3NzMgOTkuODU5NCA1OS45NDUzIDk5LjQzNzUgNTkuNzEwOUM5OS4wMTU2IDU5LjQ3NjYgOTguNTI3MyA1OS4zNTk0IDk3Ljk3MjcgNTkuMzU5NEM5Ny40MTAyIDU5LjM1OTQgOTYuOTIxOSA1OS40NzY2IDk2LjUwNzggNTkuNzEwOUM5Ni4wOTM4IDU5Ljk0NTMgOTUuNzY5NSA2MC4yNzczIDk1LjUzNTIgNjAuNzA3Qzk1LjMwODYgNjEuMTM2NyA5NS4xOTUzIDYxLjYzNjcgOTUuMTk1MyA2Mi4yMDdDOTUuMTk1MyA2Mi43ODUyIDk1LjMwODYgNjMuMjg1MiA5NS41MzUyIDYzLjcwN0M5NS43NjE3IDY0LjEyMTEgOTYuMDg1OSA2NC40Mzc1IDk2LjUwNzggNjQuNjU2MkM5Ni45Mjk3IDY0Ljg3NSA5Ny40MjU4IDY0Ljk4NDQgOTcuOTk2MSA2NC45ODQ0Qzk4LjU2NjQgNjQuOTg0NCA5OS4wNTg2IDY0Ljg3NSA5OS40NzI3IDY0LjY1NjJDOTkuODg2NyA2NC40Mzc1IDEwMC4yMDcgNjQuMTIxMSAxMDAuNDM0IDYzLjcwN0MxMDAuNjYgNjMuMjg1MiAxMDAuNzczIDYyLjc4NTIgMTAwLjc3MyA2Mi4yMDdaTTEwMy4yMjMgNTQuNDI1OEMxMDMuMjIzIDU1LjI3NzMgMTAyLjk5NiA1Ni4wMzUyIDEwMi41NDMgNTYuNjk5MkMxMDIuMDk4IDU3LjM2MzMgMTAxLjQ4IDU3Ljg4NjcgMTAwLjY5MSA1OC4yNjk1Qzk5LjkwMjMgNTguNjQ0NSA5OS4wMDM5IDU4LjgzMiA5Ny45OTYxIDU4LjgzMkM5Ni45ODA1IDU4LjgzMiA5Ni4wNzQyIDU4LjY0NDUgOTUuMjc3MyA1OC4yNjk1Qzk0LjQ4ODMgNTcuODg2NyA5My44NjcyIDU3LjM2MzMgOTMuNDE0MSA1Ni42OTkyQzkyLjk2ODggNTYuMDM1MiA5Mi43NDYxIDU1LjI3NzMgOTIuNzQ2MSA1NC40MjU4QzkyLjc0NjEgNTMuNDEwMiA5Mi45Njg4IDUyLjU1NDcgOTMuNDE0MSA1MS44NTk0QzkzLjg2NzIgNTEuMTU2MiA5NC40ODgzIDUwLjYyMTEgOTUuMjc3MyA1MC4yNTM5Qzk2LjA2NjQgNDkuODg2NyA5Ni45Njg4IDQ5LjcwMzEgOTcuOTg0NCA0OS43MDMxQzk5IDQ5LjcwMzEgOTkuOTAyMyA0OS44ODY3IDEwMC42OTEgNTAuMjUzOUMxMDEuNDggNTAuNjIxMSAxMDIuMDk4IDUxLjE1NjIgMTAyLjU0MyA1MS44NTk0QzEwMi45OTYgNTIuNTU0NyAxMDMuMjIzIDUzLjQxMDIgMTAzLjIyMyA1NC40MjU4Wk0xMDAuMzk4IDU0LjUxOTVDMTAwLjM5OCA1NC4wMTE3IDEwMC4yOTcgNTMuNTY2NCAxMDAuMDk0IDUzLjE4MzZDOTkuODk4NCA1Mi43OTMgOTkuNjIxMSA1Mi40ODgzIDk5LjI2MTcgNTIuMjY5NUM5OC45MDIzIDUyLjA1MDggOTguNDc2NiA1MS45NDE0IDk3Ljk4NDQgNTEuOTQxNEM5Ny40OTIyIDUxLjk0MTQgOTcuMDY2NCA1Mi4wNDY5IDk2LjcwNyA1Mi4yNTc4Qzk2LjM0NzcgNTIuNDY4OCA5Ni4wNzAzIDUyLjc2NTYgOTUuODc1IDUzLjE0ODRDOTUuNjc5NyA1My41MzEyIDk1LjU4MiA1My45ODgzIDk1LjU4MiA1NC41MTk1Qzk1LjU4MiA1NS4wNDMgOTUuNjc5NyA1NS41IDk1Ljg3NSA1NS44OTA2Qzk2LjA3MDMgNTYuMjczNCA5Ni4zNDc3IDU2LjU3NDIgOTYuNzA3IDU2Ljc5M0M5Ny4wNzQyIDU3LjAxMTcgOTcuNTAzOSA1Ny4xMjExIDk3Ljk5NjEgNTcuMTIxMUM5OC40ODgzIDU3LjEyMTEgOTguOTE0MSA1Ny4wMTE3IDk5LjI3MzQgNTYuNzkzQzk5LjYzMjggNTYuNTc0MiA5OS45MTAyIDU2LjI3MzQgMTAwLjEwNSA1NS44OTA2QzEwMC4zMDEgNTUuNSAxMDAuMzk4IDU1LjA0MyAxMDAuMzk4IDU0LjUxOTVaTTEwNi4wMzcgNTQuMTIxMVY1My4yMTg4QzEwNi4wMzcgNTIuNTcwMyAxMDYuMTc3IDUxLjk4MDUgMTA2LjQ1OCA1MS40NDkyQzEwNi43NCA1MC45MTggMTA3LjE1IDUwLjQ5MjIgMTA3LjY4OSA1MC4xNzE5QzEwOC4yMjggNDkuODUxNiAxMDguODc2IDQ5LjY5MTQgMTA5LjYzNCA0OS42OTE0QzExMC40MTUgNDkuNjkxNCAxMTEuMDcyIDQ5Ljg1MTYgMTExLjYwMyA1MC4xNzE5QzExMi4xNDIgNTAuNDkyMiAxMTIuNTUyIDUwLjkxOCAxMTIuODMzIDUxLjQ0OTJDMTEzLjExNSA1MS45ODA1IDExMy4yNTUgNTIuNTcwMyAxMTMuMjU1IDUzLjIxODhWNTQuMTIxMUMxMTMuMjU1IDU0Ljc1MzkgMTEzLjExNSA1NS4zMzU5IDExMi44MzMgNTUuODY3MkMxMTIuNTYgNTYuMzk4NCAxMTIuMTU0IDU2LjgyNDIgMTExLjYxNSA1Ny4xNDQ1QzExMS4wODMgNTcuNDY0OCAxMTAuNDMxIDU3LjYyNSAxMDkuNjU4IDU3LjYyNUMxMDguODkyIDU3LjYyNSAxMDguMjM2IDU3LjQ2NDggMTA3LjY4OSA1Ny4xNDQ1QzEwNy4xNSA1Ni44MjQyIDEwNi43NCA1Ni4zOTg0IDEwNi40NTggNTUuODY3MkMxMDYuMTc3IDU1LjMzNTkgMTA2LjAzNyA1NC43NTM5IDEwNi4wMzcgNTQuMTIxMVpNMTA3Ljk5NCA1My4yMTg4VjU0LjEyMTFDMTA3Ljk5NCA1NC40MzM2IDEwOC4wNTIgNTQuNzMwNSAxMDguMTY5IDU1LjAxMTdDMTA4LjI5NCA1NS4yOTMgMTA4LjQ4MiA1NS41MTk1IDEwOC43MzIgNTUuNjkxNEMxMDguOTgyIDU1Ljg2MzMgMTA5LjI5IDU1Ljk0OTIgMTA5LjY1OCA1NS45NDkyQzExMC4wMzMgNTUuOTQ5MiAxMTAuMzM3IDU1Ljg2MzMgMTEwLjU3MiA1NS42OTE0QzExMC44MTQgNTUuNTE5NSAxMTAuOTk0IDU1LjI5MyAxMTEuMTExIDU1LjAxMTdDMTExLjIyOCA1NC43MzA1IDExMS4yODcgNTQuNDMzNiAxMTEuMjg3IDU0LjEyMTFWNTMuMjE4OEMxMTEuMjg3IDUyLjg5ODQgMTExLjIyNCA1Mi41OTc3IDExMS4wOTkgNTIuMzE2NEMxMTAuOTgyIDUyLjAyNzMgMTEwLjgwMiA1MS43OTY5IDExMC41NiA1MS42MjVDMTEwLjMxOCA1MS40NTMxIDExMC4wMDkgNTEuMzY3MiAxMDkuNjM0IDUxLjM2NzJDMTA5LjI3NSA1MS4zNjcyIDEwOC45NyA1MS40NTMxIDEwOC43MiA1MS42MjVDMTA4LjQ3OCA1MS43OTY5IDEwOC4yOTQgNTIuMDI3MyAxMDguMTY5IDUyLjMxNjRDMTA4LjA1MiA1Mi41OTc3IDEwNy45OTQgNTIuODk4NCAxMDcuOTk0IDUzLjIxODhaTTExNC4yNjMgNjMuNzMwNVY2Mi44MTY0QzExNC4yNjMgNjIuMTc1OCAxMTQuNDA0IDYxLjU4OTggMTE0LjY4NSA2MS4wNTg2QzExNC45NzQgNjAuNTI3MyAxMTUuMzg4IDYwLjEwMTYgMTE1LjkyNyA1OS43ODEyQzExNi40NjYgNTkuNDYwOSAxMTcuMTE1IDU5LjMwMDggMTE3Ljg3MyA1OS4zMDA4QzExOC42NTQgNTkuMzAwOCAxMTkuMzEgNTkuNDYwOSAxMTkuODQxIDU5Ljc4MTJDMTIwLjM4IDYwLjEwMTYgMTIwLjc4NyA2MC41MjczIDEyMS4wNiA2MS4wNTg2QzEyMS4zNDEgNjEuNTg5OCAxMjEuNDgyIDYyLjE3NTggMTIxLjQ4MiA2Mi44MTY0VjYzLjczMDVDMTIxLjQ4MiA2NC4zNzExIDEyMS4zNDEgNjQuOTU3IDEyMS4wNiA2NS40ODgzQzEyMC43ODcgNjYuMDE5NSAxMjAuMzg0IDY2LjQ0NTMgMTE5Ljg1MyA2Ni43NjU2QzExOS4zMjIgNjcuMDg1OSAxMTguNjczIDY3LjI0NjEgMTE3LjkwOCA2Ny4yNDYxQzExNy4xMzQgNjcuMjQ2MSAxMTYuNDc0IDY3LjA4NTkgMTE1LjkyNyA2Ni43NjU2QzExNS4zODggNjYuNDQ1MyAxMTQuOTc0IDY2LjAxOTUgMTE0LjY4NSA2NS40ODgzQzExNC40MDQgNjQuOTU3IDExNC4yNjMgNjQuMzcxMSAxMTQuMjYzIDYzLjczMDVaTTExNi4yMzIgNjIuODE2NFY2My43MzA1QzExNi4yMzIgNjQuMDQzIDExNi4yOTQgNjQuMzM5OCAxMTYuNDE5IDY0LjYyMTFDMTE2LjU1MiA2NC45MDIzIDExNi43NDQgNjUuMTMyOCAxMTYuOTk0IDY1LjMxMjVDMTE3LjI0NCA2NS40ODQ0IDExNy41NDQgNjUuNTcwMyAxMTcuODk2IDY1LjU3MDNDMTE4LjI5NCA2NS41NzAzIDExOC42MTUgNjUuNDg0NCAxMTguODU3IDY1LjMxMjVDMTE5LjA5OSA2NS4xMzI4IDExOS4yNzEgNjQuOTA2MiAxMTkuMzczIDY0LjYzMjhDMTE5LjQ4MiA2NC4zNTE2IDExOS41MzcgNjQuMDUwOCAxMTkuNTM3IDYzLjczMDVWNjIuODE2NEMxMTkuNTM3IDYyLjQ5NjEgMTE5LjQ3NCA2Mi4xOTUzIDExOS4zNDkgNjEuOTE0MUMxMTkuMjMyIDYxLjYzMjggMTE5LjA0OCA2MS40MDYyIDExOC43OTggNjEuMjM0NEMxMTguNTU2IDYxLjA2MjUgMTE4LjI0OCA2MC45NzY2IDExNy44NzMgNjAuOTc2NkMxMTcuNTA1IDYwLjk3NjYgMTE3LjIwMSA2MS4wNjI1IDExNi45NTggNjEuMjM0NEMxMTYuNzE2IDYxLjQwNjIgMTE2LjUzMyA2MS42MzI4IDExNi40MDggNjEuOTE0MUMxMTYuMjkgNjIuMTk1MyAxMTYuMjMyIDYyLjQ5NjEgMTE2LjIzMiA2Mi44MTY0Wk0xMTguNTc2IDUyLjM3NUwxMTAuMjQ0IDY1LjcxMDlMMTA4LjgwMiA2NC44Nzg5TDExNy4xMzQgNTEuNTQzTDExOC41NzYgNTIuMzc1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cmVjdCB5PSI3OSIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI4IiByeD0iMi43MDE3NCIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjMiLz4KPGNpcmNsZSBjeD0iMi45MTk0NiIgY3k9IjgzIiByPSIwLjU2ODg3OCIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjE4LjI1MjUiIGN5PSI4MyIgcj0iMC41Njg4NzgiIGZpbGw9IiM1NDY5RkYiIGZpbGwtb3BhY2l0eT0iMC44MiIvPgo8Y2lyY2xlIGN4PSIzMy41ODQ1IiBjeT0iODMiIHI9IjAuNTY4ODc4IiBmaWxsPSIjNTQ2OUZGIiBmaWxsLW9wYWNpdHk9IjAuODIiLz4KPGNpcmNsZSBjeD0iNDguOTE2NSIgY3k9IjgzIiByPSIwLjU2ODg3OCIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjY0LjI0ODYiIGN5PSI4MyIgcj0iMC41Njg4NzgiIGZpbGw9IiM1NDY5RkYiIGZpbGwtb3BhY2l0eT0iMC44MiIvPgo8Y2lyY2xlIGN4PSI3OS41ODA2IiBjeT0iODMiIHI9IjAuNTY4ODc4IiBmaWxsPSIjNTQ2OUZGIiBmaWxsLW9wYWNpdHk9IjAuODIiLz4KPGNpcmNsZSBjeD0iOTQuOTEzNiIgY3k9IjgzIiByPSIwLjU2ODg3OCIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjExMC42NzciIGN5PSI4MyIgcj0iMSIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjEyNi44NzEiIGN5PSI4MyIgcj0iMSIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjE0My4wNjUiIGN5PSI4MyIgcj0iMSIgZmlsbD0iIzU0NjlGRiIgZmlsbC1vcGFjaXR5PSIwLjgyIi8+CjxjaXJjbGUgY3g9IjE1OS4yNiIgY3k9IjgzIiByPSIxIiBmaWxsPSIjNTQ2OUZGIiBmaWxsLW9wYWNpdHk9IjAuODIiLz4KPGNpcmNsZSBjeD0iMTc1LjQ1NSIgY3k9IjgzIiByPSIxIiBmaWxsPSIjNTQ2OUZGIiBmaWxsLW9wYWNpdHk9IjAuODIiLz4KPGNpcmNsZSBjeD0iMTkxLjY0OSIgY3k9IjgzIiByPSIxIiBmaWxsPSIjNTQ2OUZGIiBmaWxsLW9wYWNpdHk9IjAuODIiLz4KPHJlY3QgeT0iNzkiIHdpZHRoPSIxMDEiIGhlaWdodD0iOCIgcng9IjIuNzAxNzQiIGZpbGw9IiM1NDY5RkYiLz4KPHBhdGggZD0iTTUuNTU5MDggOTUuNDY2OFY5Ni42NTkyQzUuNTU5MDggOTcuMzAwMSA1LjUwMTc5IDk3Ljg0MDggNS4zODcyMSA5OC4yODEyQzUuMjcyNjIgOTguNzIxNyA1LjEwNzkxIDk5LjA3NjIgNC44OTMwNyA5OS4zNDQ3QzQuNjc4MjIgOTkuNjEzMyA0LjQxODYyIDk5LjgwODQgNC4xMTQyNiA5OS45MzAyQzMuODEzNDggMTAwLjA0OCAzLjQ3MzMxIDEwMC4xMDcgMy4wOTM3NSAxMDAuMTA3QzIuNzkyOTcgMTAwLjEwNyAyLjUxNTQ2IDEwMC4wNyAyLjI2MTIzIDk5Ljk5NDZDMi4wMDcgOTkuOTE5NCAxLjc3NzgzIDk5Ljc5OTUgMS41NzM3MyA5OS42MzQ4QzEuMzczMjEgOTkuNDY2NSAxLjIwMTMzIDk5LjI0OCAxLjA1ODExIDk4Ljk3OTVDMC45MTQ4NzYgOTguNzEwOSAwLjgwNTY2NCA5OC4zODUxIDAuNzMwNDY5IDk4LjAwMkMwLjY1NTI3MyA5Ny42MTg4IDAuNjE3Njc2IDk3LjE3MTIgMC42MTc2NzYgOTYuNjU5MlY5NS40NjY4QzAuNjE3Njc2IDk0LjgyNTggMC42NzQ5NjcgOTQuMjg4NyAwLjc4OTU1MSA5My44NTU1QzAuOTA3NzE1IDkzLjQyMjIgMS4wNzQyMiA5My4wNzQ5IDEuMjg5MDYgOTIuODEzNUMxLjUwMzkxIDkyLjU0ODUgMS43NjE3MiA5Mi4zNTg3IDIuMDYyNSA5Mi4yNDQxQzIuMzY2ODYgOTIuMTI5NiAyLjcwNzAzIDkyLjA3MjMgMy4wODMwMSA5Mi4wNzIzQzMuMzg3MzcgOTIuMDcyMyAzLjY2NjY3IDkyLjEwOTkgMy45MjA5IDkyLjE4NTFDNC4xNzg3MSA5Mi4yNTY3IDQuNDA3ODggOTIuMzczIDQuNjA4NCA5Mi41MzQyQzQuODA4OTIgOTIuNjkxNyA0Ljk3OSA5Mi45MDMgNS4xMTg2NSA5My4xNjhDNS4yNjE4OCA5My40Mjk0IDUuMzcxMDkgOTMuNzQ5OCA1LjQ0NjI5IDk0LjEyOTRDNS41MjE0OCA5NC41MDkgNS41NTkwOCA5NC45NTQ4IDUuNTU5MDggOTUuNDY2OFpNNC41NjAwNiA5Ni44MjAzVjk1LjMwMDNDNC41NjAwNiA5NC45NDk0IDQuNTM4NTcgOTQuNjQxNCA0LjQ5NTYxIDk0LjM3NjVDNC40NTYyMiA5NC4xMDc5IDQuMzk3MTQgOTMuODc4NyA0LjMxODM2IDkzLjY4OUM0LjIzOTU4IDkzLjQ5OTIgNC4xMzkzMiA5My4zNDUyIDQuMDE3NTggOTMuMjI3MUMzLjg5OTQxIDkzLjEwODkgMy43NjE1NiA5My4wMjI5IDMuNjA0IDkyLjk2OTJDMy40NTAwMyA5Mi45MTE5IDMuMjc2MzcgOTIuODgzMyAzLjA4MzAxIDkyLjg4MzNDMi44NDY2OCA5Mi44ODMzIDIuNjM3MjEgOTIuOTI4MSAyLjQ1NDU5IDkzLjAxNzZDMi4yNzE5NyA5My4xMDM1IDIuMTE4IDkzLjI0MTQgMS45OTI2OCA5My40MzEyQzEuODcwOTMgOTMuNjIwOSAxLjc3NzgzIDkzLjg2OTggMS43MTMzOCA5NC4xNzc3QzEuNjQ4OTMgOTQuNDg1NyAxLjYxNjcgOTQuODU5OSAxLjYxNjcgOTUuMzAwM1Y5Ni44MjAzQzEuNjE2NyA5Ny4xNzEyIDEuNjM2MzkgOTcuNDgxIDEuNjc1NzggOTcuNzQ5NUMxLjcxODc1IDk4LjAxODEgMS43ODE0MSA5OC4yNTA4IDEuODYzNzcgOTguNDQ3OEMxLjk0NjEzIDk4LjY0MTEgMi4wNDYzOSA5OC44MDA1IDIuMTY0NTUgOTguOTI1OEMyLjI4MjcxIDk5LjA1MTEgMi40MTg3OCA5OS4xNDQyIDIuNTcyNzUgOTkuMjA1MUMyLjczMDMxIDk5LjI2MjQgMi45MDM5NyA5OS4yOTEgMy4wOTM3NSA5OS4yOTFDMy4zMzcyNCA5OS4yOTEgMy41NTAyOSA5OS4yNDQ1IDMuNzMyOTEgOTkuMTUxNEMzLjkxNTUzIDk5LjA1ODMgNC4wNjc3MSA5OC45MTMyIDQuMTg5NDUgOTguNzE2M0M0LjMxNDc4IDk4LjUxNTggNC40MDc4OCA5OC4yNTk4IDQuNDY4NzUgOTcuOTQ4MkM0LjUyOTYyIDk3LjYzMzEgNC41NjAwNiA5Ny4yNTcyIDQuNTYwMDYgOTYuODIwM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTE4NC44NTMgOTIuMTM2N1YxMDBIMTgzLjg1OVY5My4zNzc0TDE4MS44NTYgOTQuMTA3OVY5My4yMTA5TDE4NC42OTcgOTIuMTM2N0gxODQuODUzWk0xOTIuOTM0IDk1LjQ2NjhWOTYuNjU5MkMxOTIuOTM0IDk3LjMwMDEgMTkyLjg3NyA5Ny44NDA4IDE5Mi43NjIgOTguMjgxMkMxOTIuNjQ4IDk4LjcyMTcgMTkyLjQ4MyA5OS4wNzYyIDE5Mi4yNjggOTkuMzQ0N0MxOTIuMDUzIDk5LjYxMzMgMTkxLjc5NCA5OS44MDg0IDE5MS40ODkgOTkuOTMwMkMxOTEuMTg4IDEwMC4wNDggMTkwLjg0OCAxMDAuMTA3IDE5MC40NjkgMTAwLjEwN0MxOTAuMTY4IDEwMC4xMDcgMTg5Ljg5IDEwMC4wNyAxODkuNjM2IDk5Ljk5NDZDMTg5LjM4MiA5OS45MTk0IDE4OS4xNTMgOTkuNzk5NSAxODguOTQ5IDk5LjYzNDhDMTg4Ljc0OCA5OS40NjY1IDE4OC41NzYgOTkuMjQ4IDE4OC40MzMgOTguOTc5NUMxODguMjkgOTguNzEwOSAxODguMTgxIDk4LjM4NTEgMTg4LjEwNSA5OC4wMDJDMTg4LjAzIDk3LjYxODggMTg3Ljk5MyA5Ny4xNzEyIDE4Ny45OTMgOTYuNjU5MlY5NS40NjY4QzE4Ny45OTMgOTQuODI1OCAxODguMDUgOTQuMjg4NyAxODguMTY1IDkzLjg1NTVDMTg4LjI4MyA5My40MjIyIDE4OC40NDkgOTMuMDc0OSAxODguNjY0IDkyLjgxMzVDMTg4Ljg3OSA5Mi41NDg1IDE4OS4xMzcgOTIuMzU4NyAxODkuNDM4IDkyLjI0NDFDMTg5Ljc0MiA5Mi4xMjk2IDE5MC4wODIgOTIuMDcyMyAxOTAuNDU4IDkyLjA3MjNDMTkwLjc2MiA5Mi4wNzIzIDE5MS4wNDIgOTIuMTA5OSAxOTEuMjk2IDkyLjE4NTFDMTkxLjU1NCA5Mi4yNTY3IDE5MS43ODMgOTIuMzczIDE5MS45ODMgOTIuNTM0MkMxOTIuMTg0IDkyLjY5MTcgMTkyLjM1NCA5Mi45MDMgMTkyLjQ5NCA5My4xNjhDMTkyLjYzNyA5My40Mjk0IDE5Mi43NDYgOTMuNzQ5OCAxOTIuODIxIDk0LjEyOTRDMTkyLjg5NiA5NC41MDkgMTkyLjkzNCA5NC45NTQ4IDE5Mi45MzQgOTUuNDY2OFpNMTkxLjkzNSA5Ni44MjAzVjk1LjMwMDNDMTkxLjkzNSA5NC45NDk0IDE5MS45MTQgOTQuNjQxNCAxOTEuODcxIDk0LjM3NjVDMTkxLjgzMSA5NC4xMDc5IDE5MS43NzIgOTMuODc4NyAxOTEuNjkzIDkzLjY4OUMxOTEuNjE1IDkzLjQ5OTIgMTkxLjUxNCA5My4zNDUyIDE5MS4zOTMgOTMuMjI3MUMxOTEuMjc0IDkzLjEwODkgMTkxLjEzNyA5My4wMjI5IDE5MC45NzkgOTIuOTY5MkMxOTAuODI1IDkyLjkxMTkgMTkwLjY1MSA5Mi44ODMzIDE5MC40NTggOTIuODgzM0MxOTAuMjIyIDkyLjg4MzMgMTkwLjAxMiA5Mi45MjgxIDE4OS44MyA5My4wMTc2QzE4OS42NDcgOTMuMTAzNSAxODkuNDkzIDkzLjI0MTQgMTg5LjM2OCA5My40MzEyQzE4OS4yNDYgOTMuNjIwOSAxODkuMTUzIDkzLjg2OTggMTg5LjA4OCA5NC4xNzc3QzE4OS4wMjQgOTQuNDg1NyAxODguOTkyIDk0Ljg1OTkgMTg4Ljk5MiA5NS4zMDAzVjk2LjgyMDNDMTg4Ljk5MiA5Ny4xNzEyIDE4OS4wMTEgOTcuNDgxIDE4OS4wNTEgOTcuNzQ5NUMxODkuMDk0IDk4LjAxODEgMTg5LjE1NiA5OC4yNTA4IDE4OS4yMzkgOTguNDQ3OEMxODkuMzIxIDk4LjY0MTEgMTg5LjQyMSA5OC44MDA1IDE4OS41NCA5OC45MjU4QzE4OS42NTggOTkuMDUxMSAxODkuNzk0IDk5LjE0NDIgMTg5Ljk0OCA5OS4yMDUxQzE5MC4xMDUgOTkuMjYyNCAxOTAuMjc5IDk5LjI5MSAxOTAuNDY5IDk5LjI5MUMxOTAuNzEyIDk5LjI5MSAxOTAuOTI1IDk5LjI0NDUgMTkxLjEwOCA5OS4xNTE0QzE5MS4yOTEgOTkuMDU4MyAxOTEuNDQzIDk4LjkxMzIgMTkxLjU2NCA5OC43MTYzQzE5MS42OSA5OC41MTU4IDE5MS43ODMgOTguMjU5OCAxOTEuODQ0IDk3Ljk0ODJDMTkxLjkwNSA5Ny42MzMxIDE5MS45MzUgOTcuMjU3MiAxOTEuOTM1IDk2LjgyMDNaTTE5OS4zNzIgOTUuNDY2OFY5Ni42NTkyQzE5OS4zNzIgOTcuMzAwMSAxOTkuMzE0IDk3Ljg0MDggMTk5LjIgOTguMjgxMkMxOTkuMDg1IDk4LjcyMTcgMTk4LjkyIDk5LjA3NjIgMTk4LjcwNiA5OS4zNDQ3QzE5OC40OTEgOTkuNjEzMyAxOTguMjMxIDk5LjgwODQgMTk3LjkyNyA5OS45MzAyQzE5Ny42MjYgMTAwLjA0OCAxOTcuMjg2IDEwMC4xMDcgMTk2LjkwNiAxMDAuMTA3QzE5Ni42MDUgMTAwLjEwNyAxOTYuMzI4IDEwMC4wNyAxOTYuMDc0IDk5Ljk5NDZDMTk1LjgxOSA5OS45MTk0IDE5NS41OSA5OS43OTk1IDE5NS4zODYgOTkuNjM0OEMxOTUuMTg2IDk5LjQ2NjUgMTk1LjAxNCA5OS4yNDggMTk0Ljg3MSA5OC45Nzk1QzE5NC43MjcgOTguNzEwOSAxOTQuNjE4IDk4LjM4NTEgMTk0LjU0MyA5OC4wMDJDMTk0LjQ2OCA5Ny42MTg4IDE5NC40MyA5Ny4xNzEyIDE5NC40MyA5Ni42NTkyVjk1LjQ2NjhDMTk0LjQzIDk0LjgyNTggMTk0LjQ4NyA5NC4yODg3IDE5NC42MDIgOTMuODU1NUMxOTQuNzIgOTMuNDIyMiAxOTQuODg3IDkzLjA3NDkgMTk1LjEwMiA5Mi44MTM1QzE5NS4zMTYgOTIuNTQ4NSAxOTUuNTc0IDkyLjM1ODcgMTk1Ljg3NSA5Mi4yNDQxQzE5Ni4xNzkgOTIuMTI5NiAxOTYuNTIgOTIuMDcyMyAxOTYuODk2IDkyLjA3MjNDMTk3LjIgOTIuMDcyMyAxOTcuNDc5IDkyLjEwOTkgMTk3LjczMyA5Mi4xODUxQzE5Ny45OTEgOTIuMjU2NyAxOTguMjIgOTIuMzczIDE5OC40MjEgOTIuNTM0MkMxOTguNjIxIDkyLjY5MTcgMTk4Ljc5MiA5Mi45MDMgMTk4LjkzMSA5My4xNjhDMTk5LjA3NCA5My40Mjk0IDE5OS4xODQgOTMuNzQ5OCAxOTkuMjU5IDk0LjEyOTRDMTk5LjMzNCA5NC41MDkgMTk5LjM3MiA5NC45NTQ4IDE5OS4zNzIgOTUuNDY2OFpNMTk4LjM3MyA5Ni44MjAzVjk1LjMwMDNDMTk4LjM3MyA5NC45NDk0IDE5OC4zNTEgOTQuNjQxNCAxOTguMzA4IDk0LjM3NjVDMTk4LjI2OSA5NC4xMDc5IDE5OC4yMSA5My44Nzg3IDE5OC4xMzEgOTMuNjg5QzE5OC4wNTIgOTMuNDk5MiAxOTcuOTUyIDkzLjM0NTIgMTk3LjgzIDkzLjIyNzFDMTk3LjcxMiA5My4xMDg5IDE5Ny41NzQgOTMuMDIyOSAxOTcuNDE3IDkyLjk2OTJDMTk3LjI2MyA5Mi45MTE5IDE5Ny4wODkgOTIuODgzMyAxOTYuODk2IDkyLjg4MzNDMTk2LjY1OSA5Mi44ODMzIDE5Ni40NSA5Mi45MjgxIDE5Ni4yNjcgOTMuMDE3NkMxOTYuMDg0IDkzLjEwMzUgMTk1LjkzMSA5My4yNDE0IDE5NS44MDUgOTMuNDMxMkMxOTUuNjgzIDkzLjYyMDkgMTk1LjU5IDkzLjg2OTggMTk1LjUyNiA5NC4xNzc3QzE5NS40NjEgOTQuNDg1NyAxOTUuNDI5IDk0Ljg1OTkgMTk1LjQyOSA5NS4zMDAzVjk2LjgyMDNDMTk1LjQyOSA5Ny4xNzEyIDE5NS40NDkgOTcuNDgxIDE5NS40ODggOTcuNzQ5NUMxOTUuNTMxIDk4LjAxODEgMTk1LjU5NCA5OC4yNTA4IDE5NS42NzYgOTguNDQ3OEMxOTUuNzU5IDk4LjY0MTEgMTk1Ljg1OSA5OC44MDA1IDE5NS45NzcgOTguOTI1OEMxOTYuMDk1IDk5LjA1MTEgMTk2LjIzMSA5OS4xNDQyIDE5Ni4zODUgOTkuMjA1MUMxOTYuNTQzIDk5LjI2MjQgMTk2LjcxNiA5OS4yOTEgMTk2LjkwNiA5OS4yOTFDMTk3LjE1IDk5LjI5MSAxOTcuMzYzIDk5LjI0NDUgMTk3LjU0NSA5OS4xNTE0QzE5Ny43MjggOTkuMDU4MyAxOTcuODggOTguOTEzMiAxOTguMDAyIDk4LjcxNjNDMTk4LjEyNyA5OC41MTU4IDE5OC4yMiA5OC4yNTk4IDE5OC4yODEgOTcuOTQ4MkMxOTguMzQyIDk3LjYzMzEgMTk4LjM3MyA5Ny4yNTcyIDE5OC4zNzMgOTYuODIwM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iODMiIHI9IjgiIGZpbGw9IiM1NDY5RkYiLz4KPC9zdmc+Cg==", + "description": "Allows users to move thumb of a slider to send commands to devices or update attributes/time-series data. Configurable settings let users define how to retrieve the initial state and specify actions for the value change.", + "descriptor": { + "type": "rpc", + "sizeX": 5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '360px',\n previewHeight: '220px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-slider-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-slider-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Slider\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + }, + "tags": [ + "command", + "downlink", + "device configuration", + "device control", + "invocation", + "remote method", + "remote function", + "interface", + "subroutine call", + "inter-process communication", + "server request", + "update attribute", + "set attribute", + "add time-series", + "slider" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 0ddfc98705..4ccee86d5e 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -2317,7 +2317,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "45e4507d-3adc-bb31-8b2b-1ba09bbd56ac" @@ -2484,7 +2484,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "852eccce-98eb-24db-c783-bdd62566f906" @@ -2650,7 +2650,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "3c31ba62-e760-2bea-4c8d-d32784a86c24" @@ -2816,7 +2816,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "4b55ea81-93bf-4206-9166-3e0bdc1dd9f3" @@ -2982,7 +2982,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "babf88d0-a118-e2b5-f10e-3a5970c8a65b" @@ -3148,7 +3148,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "94de7690-f91d-b032-6771-85af99abd749" @@ -3314,7 +3314,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "18414f44-1c65-536a-14de-eaf21a7d56bd" @@ -3480,7 +3480,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "794974da-c9d2-a9f7-be47-c9eb642094e8" @@ -3646,7 +3646,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "2add705b-3e53-8559-8126-380cac686fb0" @@ -3812,7 +3812,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "7e1ba820-9992-d52a-579b-20485abb3926" @@ -3978,7 +3978,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "91af27c1-b37c-2276-6022-a332e41b2b33" @@ -4144,7 +4144,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "26cf8696-054b-13ec-7984-6fc5df20e6f1" @@ -4310,7 +4310,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "1dcfaf24-32be-cd19-62d6-86d12cc6a7ef" @@ -4476,7 +4476,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "ad2bc817-f3c4-150c-4672-8fe0c38aee8d" @@ -4642,7 +4642,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "d1ad84cd-bd9c-4dca-e4a0-f444ae8598bd" @@ -4808,7 +4808,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "bf80eef9-b879-9a08-40a4-488dbdefa125" @@ -4974,7 +4974,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "b5a406b3-cc0a-8a09-9aec-3f8befae5fb8" @@ -5140,7 +5140,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "custom", - "customFunction": "const url = `${window.location.origin}/entities/devices/${entityId.id}`;\nwindow.open(url, '_blank');", + "customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');", "openInSeparateDialog": false, "openInPopover": false, "id": "ec1dfba3-4b43-2491-8948-f602337f8a3b" diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 31d9e477ac..e3ebbdf892 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -31,6 +31,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -203,6 +204,10 @@ public class ActorSystemContext { @Getter private DeviceCredentialsService deviceCredentialsService; + @Autowired(required = false) + @Getter + private RuleEngineDeviceStateManager deviceStateManager; + @Autowired @Getter private TbTenantProfileCache tenantProfileCache; @@ -556,6 +561,10 @@ public class ActorSystemContext { @Getter private boolean externalNodeForceAck; + @Value("${state.rule.node.deviceState.rateLimit:1:1,30:60,60:3600}") + @Getter + private String deviceStateNodeRateLimitConfig; + @Getter @Setter private TbActorSystem actorSystem; diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 58bd82abb3..f1c0260124 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -66,7 +66,6 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNoti import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry; @@ -90,7 +89,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; +import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMsg; import org.thingsboard.server.service.rpc.RpcSubmitStrategy; +import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import javax.annotation.Nullable; @@ -173,7 +174,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso private EdgeId findRelatedEdgeId() { List result = - systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON); + systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); if (result != null && result.size() > 0) { EntityRelation relationToEdge = result.get(0); if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { @@ -212,8 +213,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso if (systemContext.isEdgesEnabled() && edgeId != null) { log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId); try { - saveRpcRequestToEdgeQueue(request, requestId).get(); - sent = true; + if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) { + saveRpcRequestToEdgeQueue(request, requestId).get(); + } else { + log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request); + } } catch (InterruptedException | ExecutionException e) { log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); } @@ -470,7 +474,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso callback.onSuccess(); } - private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) { + private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, UplinkNotificationMsg uplinkNotificationMsg) { String nodeId = sessionInfo.getNodeId(); sessions.entrySet().stream() .filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC())) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index cc6653f4c5..9110ab794d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -108,6 +109,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine; @@ -213,7 +215,19 @@ class DefaultTbContext implements TbContext { if (nodeCtx.getSelf().isDebugMode()) { mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain"); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback(onSuccess, onFailure)); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback( + metadata -> { + if (onSuccess != null) { + onSuccess.run(); + } + }, + t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override @@ -299,7 +313,19 @@ class DefaultTbContext implements TbContext { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage)); } - mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback(onSuccess, onFailure)); + mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback( + metadata -> { + if (onSuccess != null) { + onSuccess.run(); + } + }, + t -> { + if (onFailure != null) { + onFailure.accept(t); + } else { + log.debug("[{}] Failed to put item into queue!", nodeCtx.getTenantId().getId(), t); + } + })); } @Override @@ -658,6 +684,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceCredentialsService(); } + @Override + public RuleEngineDeviceStateManager getDeviceStateManager() { + return mainCtx.getDeviceStateManager(); + } + + @Override + public String getDeviceStateNodeRateLimitConfig() { + return mainCtx.getDeviceStateNodeRateLimitConfig(); + } + @Override public TbClusterService getClusterService() { return mainCtx.getClusterService(); @@ -952,29 +988,4 @@ class DefaultTbContext implements TbContext { return failureMessage; } - private class SimpleTbQueueCallback implements TbQueueCallback { - private final Runnable onSuccess; - private final Consumer onFailure; - - public SimpleTbQueueCallback(Runnable onSuccess, Consumer onFailure) { - this.onSuccess = onSuccess; - this.onFailure = onFailure; - } - - @Override - public void onSuccess(TbQueueMsgMetadata metadata) { - if (onSuccess != null) { - onSuccess.run(); - } - } - - @Override - public void onFailure(Throwable t) { - if (onFailure != null) { - onFailure.accept(t); - } else { - log.debug("[{}] Failed to put item into queue", nodeCtx.getTenantId(), t); - } - } - } } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index e14ed8203b..870ccda60a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -16,7 +16,6 @@ package org.thingsboard.server.actors.ruleChain; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; @@ -29,6 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.relation.EntityRelation; diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 7791c7ee76..0b859e7b06 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index ba4c2b1fbd..92441bf1cb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.security.event.UserSessionInvalidation import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 1696d5c20b..64536ddb54 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -43,7 +43,7 @@ import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.model.SecurityUser; diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index e8339e3202..44d81902a8 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -126,6 +126,7 @@ public class ThingsboardInstallService { case "3.6.2": log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); + systemDataLoaderService.updateDefaultNotificationConfigs(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index a10a48244b..2f157af70e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -149,6 +150,9 @@ public class EdgeContextComponent { @Autowired private ResourceService resourceService; + @Autowired + private NotificationRuleProcessor notificationRuleProcessor; + @Autowired private AlarmEdgeProcessor alarmProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index d3262039d5..026ca51e8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -263,7 +264,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { - TenantId tenantId = edgeGrpcSession.getEdge().getTenantId(); + Edge edge = edgeGrpcSession.getEdge(); + TenantId tenantId = edge.getTenantId(); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); sessions.put(edgeId, edgeGrpcSession); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); @@ -276,7 +278,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); long lastConnectTs = System.currentTimeMillis(); save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); - pushRuleEngineMessage(tenantId, edgeId, lastConnectTs, TbMsgType.CONNECT_EVENT); + pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } @@ -381,7 +383,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void onEdgeDisconnect(EdgeId edgeId, UUID sessionId) { + private void onEdgeDisconnect(Edge edge, UUID sessionId) { + EdgeId edgeId = edge.getId(); log.info("[{}][{}] edge disconnected!", edgeId, sessionId); EdgeGrpcSession toRemove = sessions.get(edgeId); if (toRemove.getSessionId().equals(sessionId)) { @@ -397,7 +400,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); - pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); + pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } else { log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); @@ -452,25 +455,36 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, TbMsgType msgType) { + private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) { try { + EdgeId edgeId = edge.getId(); ObjectNode edgeState = JacksonUtil.newObjectNode(); - if (msgType.equals(TbMsgType.CONNECT_EVENT)) { + boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType); + if (isConnected) { edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); } else { edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false); edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts); } + ctx.getNotificationRuleProcessor().process(EdgeConnectionTrigger.builder() + .tenantId(tenantId) + .customerId(edge.getCustomerId()) + .edgeId(edgeId) + .edgeName(edge.getName()) + .connected(isConnected).build()); String data = JacksonUtil.toString(edgeState); TbMsgMetaData md = new TbMsgMetaData(); if (!persistToTelemetry) { md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); + md.putValue("edgeName", edge.getName()); + md.putValue("edgeType", edge.getType()); } TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); } catch (Exception e) { - log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e); + log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 5e6f7a263e..98346674ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; @@ -111,7 +112,7 @@ public final class EdgeGrpcSession implements Closeable { private final UUID sessionId; private final BiConsumer sessionOpenListener; - private final BiConsumer sessionCloseListener; + private final BiConsumer sessionCloseListener; private final EdgeSessionState sessionState = new EdgeSessionState(); @@ -137,7 +138,7 @@ public final class EdgeGrpcSession implements Closeable { private ScheduledExecutorService sendDownlinkExecutorService; EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, - BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { + BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { this.sessionId = UUID.randomUUID(); this.ctx = ctx; this.outputStream = outputStream; @@ -206,7 +207,7 @@ public final class EdgeGrpcSession implements Closeable { connected = false; if (edge != null) { try { - sessionCloseListener.accept(edge.getId(), sessionId); + sessionCloseListener.accept(edge, sessionId); } catch (Exception ignored) { } } @@ -314,7 +315,7 @@ public final class EdgeGrpcSession implements Closeable { } catch (Exception e) { log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e); connected = false; - sessionCloseListener.accept(edge.getId(), sessionId); + sessionCloseListener.accept(edge, sessionId); } finally { downlinkMsgLock.unlock(); } @@ -466,15 +467,26 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { - log.warn("[{}][{}] Failed to deliver the batch: {}, attempt: {}", this.tenantId, this.sessionId, copy, attempt); + String error = "Failed to deliver the batch"; + String failureMsg = String.format("{%s}: {%s}", error, copy); + if (attempt == 2) { + // Send a failure notification only on the second attempt. + // This ensures that failure alerts are sent just once to avoid redundant notifications. + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) + .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); + } + log.warn("[{}][{}] {}, attempt: {}", this.tenantId, this.sessionId, failureMsg, attempt); } log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size()); for (DownlinkMsg downlinkMsg : copy) { if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { - log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " + - "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." + - "Message {}", this.tenantId, edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), - this.clientMaxInboundMessageSize, downlinkMsg); + String error = String.format("Client max inbound message size [{%s}] is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + + "env variable on the edge and restart it.", this.clientMaxInboundMessageSize); + String message = String.format("Downlink msg size [{%s}] exceeds client max inbound message size [{%s}]. " + + "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), this.clientMaxInboundMessageSize); + log.error("[{}][{}][{}] {} Message {}", this.tenantId, edge.getId(), this.sessionId, message, downlinkMsg); + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) + .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); } else { sendDownlinkMsg(ResponseMsg.newBuilder() @@ -485,8 +497,12 @@ public final class EdgeGrpcSession implements Closeable { if (attempt < MAX_DOWNLINK_ATTEMPTS) { scheduleDownlinkMsgsPackSend(attempt + 1); } else { + String failureMsg = String.format("Failed to deliver messages: %s", copy); log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) + .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); stopCurrentSendDownlinkMsgsTask(false); } } else { @@ -791,7 +807,10 @@ public final class EdgeGrpcSession implements Closeable { } } } catch (Exception e) { + String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e); + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); return Futures.immediateFailedFuture(e); } return Futures.allAsList(result); @@ -815,15 +834,22 @@ public final class EdgeGrpcSession implements Closeable { .setMaxInboundMessageSize(maxInboundMessageSize) .build(); } + String error = "Failed to validate the edge!"; + String failureMsg = String.format("{%s} Provided request secret: %s", error, request.getEdgeSecret()); + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) - .setErrorMsg("Failed to validate the edge!") + .setErrorMsg(failureMsg) .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); } catch (Exception e) { - log.error("[{}] Failed to process edge connection!", request.getEdgeRoutingKey(), e); + String failureMsg = "Failed to process edge connection!"; + ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) + .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); + log.error(failureMsg, e); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) - .setErrorMsg("Failed to process edge connection!") + .setErrorMsg(failureMsg) .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 2a191dfaf4..118cc0ccd4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc.processor.rule; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; @@ -53,6 +54,10 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { isRoot = Boolean.parseBoolean(edgeEvent.getBody().get(EDGE_IS_ROOT_BODY_KEY).asText()); } catch (Exception ignored) {} } + if (!isRoot) { + Edge edge = edgeService.findEdgeById(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); + isRoot = edge.getRootRuleChainId().equals(ruleChainId); + } UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java index cbef97d134..f58a55678a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java @@ -50,22 +50,15 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb public Queue saveQueue(Queue queue) { boolean create = queue.getId() == null; Queue oldQueue; - if (create) { oldQueue = null; } else { oldQueue = queueService.findQueueById(queue.getTenantId(), queue.getId()); } - //TODO: add checkNotNull Queue savedQueue = queueService.saveQueue(queue); - - if (create) { - onQueueCreated(savedQueue); - } else { - onQueueUpdated(savedQueue, oldQueue); - } - + createTopicsIfNeeded(savedQueue, oldQueue); + tbClusterService.onQueuesUpdate(List.of(savedQueue)); return savedQueue; } @@ -73,54 +66,14 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb public void deleteQueue(TenantId tenantId, QueueId queueId) { Queue queue = queueService.findQueueById(tenantId, queueId); queueService.deleteQueue(tenantId, queueId); - onQueueDeleted(queue); + tbClusterService.onQueuesDelete(List.of(queue)); } @Override public void deleteQueueByQueueName(TenantId tenantId, String queueName) { Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, queueName); queueService.deleteQueue(tenantId, queue.getId()); - onQueueDeleted(queue); - } - - private void onQueueCreated(Queue queue) { - for (int i = 0; i < queue.getPartitions(); i++) { - tbQueueAdmin.createTopicIfNotExists( - new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), - queue.getCustomProperties() - ); - } - - tbClusterService.onQueueChange(queue); - } - - private void onQueueUpdated(Queue queue, Queue oldQueue) { - int oldPartitions = oldQueue.getPartitions(); - int currentPartitions = queue.getPartitions(); - - if (currentPartitions != oldPartitions) { - if (currentPartitions > oldPartitions) { - log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); - for (int i = oldPartitions; i < currentPartitions; i++) { - tbQueueAdmin.createTopicIfNotExists( - new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), - queue.getCustomProperties() - ); - } - tbClusterService.onQueueChange(queue); - } else { - log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); - tbClusterService.onQueueChange(queue); - // TODO: move all the messages left in old partitions and delete topics - } - } else if (!oldQueue.equals(queue)) { - tbClusterService.onQueueChange(queue); - } - } - - private void onQueueDeleted(Queue queue) { - tbClusterService.onQueueDelete(queue); -// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); + tbClusterService.onQueuesDelete(List.of(queue)); } @Override @@ -176,26 +129,56 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb log.debug("[{}] Handling profile queue config update: creating queues {}, updating {}, deleting {}. Affected tenants: {}", newTenantProfile.getUuidId(), toCreate, toUpdate, toRemove, tenantIds); } - tenantIds.forEach(tenantId -> { - toCreate.forEach(key -> saveQueue(new Queue(tenantId, newQueues.get(key)))); - toUpdate.forEach(key -> { - Queue queueToUpdate = new Queue(tenantId, newQueues.get(key)); - Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, key); - queueToUpdate.setId(foundQueue.getId()); - queueToUpdate.setCreatedTime(foundQueue.getCreatedTime()); + List updated = new ArrayList<>(); + List deleted = new ArrayList<>(); + for (TenantId tenantId : tenantIds) { + for (String name : toCreate) { + updated.add(new Queue(tenantId, newQueues.get(name))); + } - if (!queueToUpdate.equals(foundQueue)) { - saveQueue(queueToUpdate); + for (String name : toUpdate) { + Queue queue = new Queue(tenantId, newQueues.get(name)); + Queue foundQueue = queueService.findQueueByTenantIdAndName(tenantId, name); + if (foundQueue != null) { + queue.setId(foundQueue.getId()); + queue.setCreatedTime(foundQueue.getCreatedTime()); } - }); + if (!queue.equals(foundQueue)) { + updated.add(queue); + createTopicsIfNeeded(queue, foundQueue); + } + } + + for (String name : toRemove) { + Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, name); + deleted.add(queue); + } + } - toRemove.forEach(q -> { - Queue queue = queueService.findQueueByTenantIdAndNameInternal(tenantId, q); - QueueId queueIdForRemove = queue.getId(); - deleteQueue(tenantId, queueIdForRemove); + if (!updated.isEmpty()) { + updated = updated.stream() + .map(queueService::saveQueue) + .collect(Collectors.toList()); + tbClusterService.onQueuesUpdate(updated); + } + if (!deleted.isEmpty()) { + deleted.forEach(queue -> { + queueService.deleteQueue(queue.getTenantId(), queue.getId()); }); - }); + tbClusterService.onQueuesDelete(deleted); + } + } + + private void createTopicsIfNeeded(Queue queue, Queue oldQueue) { + int newPartitions = queue.getPartitions(); + int oldPartitions = oldQueue != null ? oldQueue.getPartitions() : 0; + for (int i = oldPartitions; i < newPartitions; i++) { + tbQueueAdmin.createTopicIfNotExists( + new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), + queue.getCustomProperties() + ); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index b9f4dc482f..b20a548d90 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -691,7 +691,23 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } @Override + @SneakyThrows public void updateDefaultNotificationConfigs() { + PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500); + ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4)); + log.info("Updating default edge failure notification configs for all tenants"); + AtomicInteger count = new AtomicInteger(); + for (TenantId tenantId : tenants) { + executor.submit(() -> { + notificationSettingsService.updateDefaultNotificationConfigs(tenantId); + int n = count.incrementAndGet(); + if (n % 500 == 0) { + log.info("{} tenants processed", n); + } + }); + } + executor.shutdown(); + executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index be37d98bb5..11c40d964a 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -62,7 +62,7 @@ import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TopicService; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index a68708d8ae..a86315ce86 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.notification.NotificationRequestService; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.notification.NotificationDeduplicationService; import org.thingsboard.server.service.executors.NotificationExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java new file mode 100644 index 0000000000..38d8ae9805 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.notification.info.EdgeCommunicationFailureNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +@Service +@RequiredArgsConstructor +public class EdgeCommunicationFailureTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(EdgeCommunicationFailureTrigger trigger, EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig) { + if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) { + return !triggerConfig.getEdges().contains(trigger.getEdgeId().getId()); + } + return true; + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeCommunicationFailureTrigger trigger) { + return EdgeCommunicationFailureNotificationInfo.builder() + .tenantId(trigger.getTenantId()) + .edgeId(trigger.getEdgeId()) + .customerId(trigger.getCustomerId()) + .edgeName(trigger.getEdgeName()) + .failureMsg(truncateFailureMsg(trigger.getFailureMsg())) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE; + } + + private String truncateFailureMsg(String input) { + int maxLength = 500; + if (input != null && input.length() > maxLength) { + return input.substring(0, maxLength); + } + return input; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java new file mode 100644 index 0000000000..ed1ee74622 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.notification.rule.trigger; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.notification.info.EdgeConnectionNotificationInfo; +import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +@Service +@RequiredArgsConstructor +public class EdgeConnectionTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(EdgeConnectionTrigger trigger, EdgeConnectionNotificationRuleTriggerConfig triggerConfig) { + EdgeConnectivityEvent event = trigger.isConnected() ? EdgeConnectivityEvent.CONNECTED : EdgeConnectivityEvent.DISCONNECTED; + if (CollectionUtils.isEmpty(triggerConfig.getNotifyOn()) || !triggerConfig.getNotifyOn().contains(event)) { + return false; + } + if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) { + return triggerConfig.getEdges().contains(trigger.getEdgeId().getId()); + } + return true; + } + + @Override + public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeConnectionTrigger trigger) { + return EdgeConnectionNotificationInfo.builder() + .eventType(trigger.isConnected() ? "connected" : "disconnected") + .tenantId(trigger.getTenantId()) + .customerId(trigger.getCustomerId()) + .edgeId(trigger.getEdgeId()) + .edgeName(trigger.getEdgeName()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_CONNECTION; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index ee05e82900..75e9e0ae2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -62,6 +62,8 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; +import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; +import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; @@ -81,9 +83,11 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Optional; +import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import static org.thingsboard.server.common.util.ProtoUtils.toProto; @@ -563,40 +567,40 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onQueueChange(Queue queue) { - log.trace("[{}][{}] Processing queue change [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); - - TransportProtos.QueueUpdateMsg queueUpdateMsg = TransportProtos.QueueUpdateMsg.newBuilder() - .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) - .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) - .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) - .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) - .setQueueName(queue.getName()) - .setQueueTopic(queue.getTopic()) - .setPartitions(queue.getPartitions()) - .build(); - - ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); - ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueUpdateMsg(queueUpdateMsg).build(); + public void onQueuesUpdate(List queues) { + List queueUpdateMsgs = queues.stream() + .map(queue -> QueueUpdateMsg.newBuilder() + .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) + .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) + .setQueueName(queue.getName()) + .setQueueTopic(queue.getTopic()) + .setPartitions(queue.getPartitions()) + .build()) + .collect(Collectors.toList()); + + ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build(); + ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().addAllQueueUpdateMsgs(queueUpdateMsgs).build(); doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg); } @Override - public void onQueueDelete(Queue queue) { - log.trace("[{}][{}] Processing queue delete [{}] event", queue.getTenantId(), queue.getId(), queue.getName()); - - TransportProtos.QueueDeleteMsg queueDeleteMsg = TransportProtos.QueueDeleteMsg.newBuilder() - .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) - .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) - .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) - .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) - .setQueueName(queue.getName()) - .build(); - - ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); - ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setQueueDeleteMsg(queueDeleteMsg).build(); + public void onQueuesDelete(List queues) { + List queueDeleteMsgs = queues.stream() + .map(queue -> QueueDeleteMsg.newBuilder() + .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) + .setQueueIdMSB(queue.getId().getId().getMostSignificantBits()) + .setQueueIdLSB(queue.getId().getId().getLeastSignificantBits()) + .setQueueName(queue.getName()) + .build()) + .collect(Collectors.toList()); + + ToRuleEngineNotificationMsg ruleEngineMsg = ToRuleEngineNotificationMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build(); + ToCoreNotificationMsg coreMsg = ToCoreNotificationMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().addAllQueueDeleteMsgs(queueDeleteMsgs).build(); doSendQueueNotifications(ruleEngineMsg, coreMsg, transportMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 2d5e6963eb..5f1bc0c8ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.service.queue; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -148,6 +151,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService 0) { + partitionService.updateQueues(toCoreNotification.getQueueUpdateMsgsList()); callback.onSuccess(); - } else if (toCoreNotification.hasQueueDeleteMsg()) { - TransportProtos.QueueDeleteMsg queue = toCoreNotification.getQueueDeleteMsg(); - partitionService.removeQueue(queue); + } else if (toCoreNotification.getQueueDeleteMsgsCount() > 0) { + partitionService.removeQueues(toCoreNotification.getQueueDeleteMsgsList()); callback.onSuccess(); } else if (toCoreNotification.hasVcResponseMsg()) { vcQueueService.processResponse(toCoreNotification.getVcResponseMsg()); @@ -643,25 +658,71 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceConnect(tenantId, deviceId, deviceConnectMsg.getLastConnectTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device connect message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); + } + + void forwardToStateService(TransportProtos.DeviceActivityProto deviceActivityMsg, TbCallback callback) { if (statsEnabled) { stats.log(deviceActivityMsg); } - TenantId tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB()); - DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB())); - try { - stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime()); - callback.onSuccess(); - } catch (Exception e) { - callback.onFailure(new RuntimeException("Failed update device activity for device [" + deviceId.getId() + "]!", e)); + var tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB()); + var deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB())); + ListenableFuture future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device activity message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(new RuntimeException("Failed to update device activity for device [" + deviceId.getId() + "]!", t)); + }); + } + + void forwardToStateService(TransportProtos.DeviceDisconnectProto deviceDisconnectMsg, TbCallback callback) { + if (statsEnabled) { + stats.log(deviceDisconnectMsg); + } + var tenantId = toTenantId(deviceDisconnectMsg.getTenantIdMSB(), deviceDisconnectMsg.getTenantIdLSB()); + var deviceId = new DeviceId(new UUID(deviceDisconnectMsg.getDeviceIdMSB(), deviceDisconnectMsg.getDeviceIdLSB())); + ListenableFuture future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceDisconnect(tenantId, deviceId, deviceDisconnectMsg.getLastDisconnectTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device disconnect message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); + } + + void forwardToStateService(TransportProtos.DeviceInactivityProto deviceInactivityMsg, TbCallback callback) { + if (statsEnabled) { + stats.log(deviceInactivityMsg); } + var tenantId = toTenantId(deviceInactivityMsg.getTenantIdMSB(), deviceInactivityMsg.getTenantIdLSB()); + var deviceId = new DeviceId(new UUID(deviceInactivityMsg.getDeviceIdMSB(), deviceInactivityMsg.getDeviceIdLSB())); + ListenableFuture future = deviceActivityEventsExecutor.submit(() -> stateService.onDeviceInactivity(tenantId, deviceId, deviceInactivityMsg.getLastInactivityTime())); + DonAsynchron.withCallback(future, + __ -> callback.onSuccess(), + t -> { + log.warn("[{}] Failed to process device inactivity message for device [{}]", tenantId.getId(), deviceId.getId(), t); + callback.onFailure(t); + }); } private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 4ebce20b32..04f57bf54d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -36,6 +36,8 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; +import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; @@ -164,11 +166,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< , proto.getResponse(), error); tbDeviceRpcService.processRpcResponseFromDevice(response); callback.onSuccess(); - } else if (nfMsg.hasQueueUpdateMsg()) { - updateQueue(nfMsg.getQueueUpdateMsg()); + } else if (nfMsg.getQueueUpdateMsgsCount() > 0) { + updateQueues(nfMsg.getQueueUpdateMsgsList()); callback.onSuccess(); - } else if (nfMsg.hasQueueDeleteMsg()) { - deleteQueue(nfMsg.getQueueDeleteMsg()); + } else if (nfMsg.getQueueDeleteMsgsCount() > 0) { + deleteQueues(nfMsg.getQueueDeleteMsgsList()); callback.onSuccess(); } else { log.trace("Received notification with missing handler"); @@ -176,39 +178,48 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } } - private void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { - log.info("Received queue update msg: [{}]", queueUpdateMsg); - TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); - if (partitionService.isManagedByCurrentService(tenantId)) { - QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); - String queueName = queueUpdateMsg.getQueueName(); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId); - Queue queue = queueService.findQueueById(tenantId, queueId); - - TbRuleEngineQueueConsumerManager consumerManager = getOrCreateConsumer(queueKey); - Queue oldQueue = consumerManager.getQueue(); - consumerManager.update(queue); - - if (oldQueue != null && queue.getPartitions() == oldQueue.getPartitions()) { - return; + private void updateQueues(List queueUpdateMsgs) { + boolean partitionsChanged = false; + for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { + log.info("Received queue update msg: [{}]", queueUpdateMsg); + TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); + if (partitionService.isManagedByCurrentService(tenantId)) { + QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); + String queueName = queueUpdateMsg.getQueueName(); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueName, tenantId); + Queue queue = queueService.findQueueById(tenantId, queueId); + + TbRuleEngineQueueConsumerManager consumerManager = getOrCreateConsumer(queueKey); + Queue oldQueue = consumerManager.getQueue(); + consumerManager.update(queue); + + if (oldQueue == null || queue.getPartitions() != oldQueue.getPartitions()) { + partitionsChanged = true; + } + } else { + partitionsChanged = true; } } - partitionService.updateQueue(queueUpdateMsg); - partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), - new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE))); + if (partitionsChanged) { + partitionService.updateQueues(queueUpdateMsgs); + partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), + new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE))); + } } - private void deleteQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { - log.info("Received queue delete msg: [{}]", queueDeleteMsg); - TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); - var consumerManager = consumers.remove(queueKey); - if (consumerManager != null) { - consumerManager.delete(true); + private void deleteQueues(List queueDeleteMsgs) { + for (QueueDeleteMsg queueDeleteMsg : queueDeleteMsgs) { + log.info("Received queue delete msg: [{}]", queueDeleteMsg); + TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + var consumerManager = consumers.remove(queueKey); + if (consumerManager != null) { + consumerManager.delete(true); + } } - partitionService.removeQueue(queueDeleteMsg); + partitionService.removeQueues(queueDeleteMsgs); partitionService.recalculatePartitions(ctx.getServiceInfoProvider().getServiceInfo(), new ArrayList<>(partitionService.getOtherServices(ServiceType.TB_RULE_ENGINE))); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java index 0fa15ad0b3..76175d86b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java @@ -37,7 +37,10 @@ public class TbCoreConsumerStats { public static final String DEVICE_STATES = "deviceState"; public static final String SUBSCRIPTION_MSGS = "subMsgs"; public static final String EDGE_NOTIFICATIONS = "edgeNfs"; + public static final String DEVICE_CONNECTS = "deviceConnect"; public static final String DEVICE_ACTIVITIES = "deviceActivity"; + public static final String DEVICE_DISCONNECTS = "deviceDisconnect"; + public static final String DEVICE_INACTIVITIES = "deviceInactivity"; public static final String TO_CORE_NF_OTHER = "coreNfOther"; // normally, there is no messages when codebase is fine public static final String TO_CORE_NF_COMPONENT_LIFECYCLE = "coreNfCompLfcl"; @@ -63,7 +66,10 @@ public class TbCoreConsumerStats { private final StatsCounter deviceStateCounter; private final StatsCounter subscriptionMsgCounter; private final StatsCounter edgeNotificationsCounter; + private final StatsCounter deviceConnectsCounter; private final StatsCounter deviceActivitiesCounter; + private final StatsCounter deviceDisconnectsCounter; + private final StatsCounter deviceInactivitiesCounter; private final StatsCounter toCoreNfOtherCounter; private final StatsCounter toCoreNfComponentLifecycleCounter; @@ -94,7 +100,10 @@ public class TbCoreConsumerStats { this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); + this.deviceConnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CONNECTS)); this.deviceActivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_ACTIVITIES)); + this.deviceDisconnectsCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_DISCONNECTS)); + this.deviceInactivitiesCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_INACTIVITIES)); // Core notification counters this.toCoreNfOtherCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NF_OTHER)); @@ -152,11 +161,26 @@ public class TbCoreConsumerStats { edgeNotificationsCounter.increment(); } + public void log(TransportProtos.DeviceConnectProto msg) { + totalCounter.increment(); + deviceConnectsCounter.increment(); + } + public void log(TransportProtos.DeviceActivityProto msg) { totalCounter.increment(); deviceActivitiesCounter.increment(); } + public void log(TransportProtos.DeviceDisconnectProto msg) { + totalCounter.increment(); + deviceDisconnectsCounter.increment(); + } + + public void log(TransportProtos.DeviceInactivityProto msg) { + totalCounter.increment(); + deviceInactivitiesCounter.increment(); + } + public void log(TransportProtos.SubscriptionMgrMsgProto msg) { totalCounter.increment(); subscriptionMsgCounter.increment(); @@ -184,9 +208,9 @@ public class TbCoreConsumerStats { toCoreNfEdgeSyncResponseCounter.increment(); } else if (!msg.getFromEdgeSyncResponseMsg().isEmpty()) { toCoreNfEdgeSyncResponseCounter.increment(); - } else if (msg.hasQueueUpdateMsg()) { + } else if (msg.getQueueUpdateMsgsCount() > 0) { toCoreNfQueueUpdateCounter.increment(); - } else if (msg.hasQueueDeleteMsg()) { + } else if (msg.getQueueDeleteMsgsCount() > 0) { toCoreNfQueueDeleteCounter.increment(); } else if (msg.hasVcResponseMsg()) { toCoreNfVersionControlResponseCounter.increment(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java index d7ddd5d81b..1f9e85bdc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.TbMsgCallback; -import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import java.util.UUID; import java.util.concurrent.TimeUnit; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java index 0205059225..1ff7ba433b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractTbRuleEngineSubmitStrategy.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.queue.processing; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -51,7 +52,16 @@ public abstract class AbstractTbRuleEngineSubmitStrategy implements TbRuleEngine List> newOrderedMsgList = new ArrayList<>(reprocessMap.size()); for (IdMsgPair pair : orderedMsgList) { if (reprocessMap.containsKey(pair.uuid)) { - newOrderedMsgList.add(pair); + if (StringUtils.isNotEmpty(pair.getMsg().getValue().getFailureMessage())) { + var toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder(pair.getMsg().getValue()) + .clearFailureMessage() + .clearRelationTypes() + .build(); + var newMsg = new TbProtoQueueMsg<>(pair.getMsg().getKey(), toRuleEngineMsg, pair.getMsg().getHeaders()); + newOrderedMsgList.add(new IdMsgPair<>(pair.getUuid(), newMsg)); + } else { + newOrderedMsgList.add(pair); + } } } orderedMsgList = newOrderedMsgList; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index b2ebcb0b5d..d4425372e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index e010cf3050..30c59b3da8 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -221,25 +222,35 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) { updateActivityState(deviceId, stateData, lastReportedActivity); @@ -261,37 +272,75 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService kvEntries, String attributeName, long defaultValue) { if (kvEntries != null) { for (KvEntry entry : kvEntries) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..af2b03ec8f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManager.java @@ -0,0 +1,196 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.state; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.TbRuleEngineComponent; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +@TbRuleEngineComponent +public class DefaultRuleEngineDeviceStateManager implements RuleEngineDeviceStateManager { + + private final TbServiceInfoProvider serviceInfoProvider; + private final PartitionService partitionService; + + private final Optional deviceStateService; + private final TbClusterService clusterService; + + public DefaultRuleEngineDeviceStateManager( + TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, + Optional deviceStateServiceOptional, TbClusterService clusterService + ) { + this.serviceInfoProvider = serviceInfoProvider; + this.partitionService = partitionService; + this.deviceStateService = deviceStateServiceOptional; + this.clusterService = clusterService; + } + + @Getter + private abstract static class ConnectivityEventInfo { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final long eventTime; + + private ConnectivityEventInfo(TenantId tenantId, DeviceId deviceId, long eventTime) { + this.tenantId = tenantId; + this.deviceId = deviceId; + this.eventTime = eventTime; + } + + abstract void forwardToLocalService(); + + abstract TransportProtos.ToCoreMsg toQueueMsg(); + + } + + @Override + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, connectTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceConnect(tenantId, deviceId, connectTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(connectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, activityTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceActivity(tenantId, deviceId, activityTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(activityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, disconnectTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceDisconnect(tenantId, deviceId, disconnectTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(disconnectTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + } + }, callback); + } + + @Override + public void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback) { + routeEvent(new ConnectivityEventInfo(tenantId, deviceId, inactivityTime) { + @Override + void forwardToLocalService() { + deviceStateService.ifPresent(service -> service.onDeviceInactivity(tenantId, deviceId, inactivityTime)); + } + + @Override + TransportProtos.ToCoreMsg toQueueMsg() { + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(inactivityTime) + .build(); + return TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + } + }, callback); + } + + private void routeEvent(ConnectivityEventInfo eventInfo, TbCallback callback) { + var tenantId = eventInfo.getTenantId(); + var deviceId = eventInfo.getDeviceId(); + long eventTime = eventInfo.getEventTime(); + + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); + if (serviceInfoProvider.isService(ServiceType.TB_CORE) && tpi.isMyPartition() && deviceStateService.isPresent()) { + log.debug("[{}][{}] Forwarding device connectivity event to local service. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + try { + eventInfo.forwardToLocalService(); + } catch (Exception e) { + log.error("[{}][{}] Failed to process device connectivity event. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime, e); + callback.onFailure(e); + return; + } + callback.onSuccess(); + } else { + TransportProtos.ToCoreMsg msg = eventInfo.toQueueMsg(); + log.debug("[{}][{}] Sending device connectivity message to core. Event time: [{}].", tenantId.getId(), deviceId.getId(), eventTime); + clusterService.pushMsgToCore(tpi, UUID.randomUUID(), msg, new SimpleTbQueueCallback(__ -> callback.onSuccess(), callback::onFailure)); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java index aa12b397a4..3ab5d29194 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java @@ -27,11 +27,21 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; */ public interface DeviceStateService extends ApplicationListener { - void onDeviceConnect(TenantId tenantId, DeviceId deviceId); + void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long lastConnectTime); + + default void onDeviceConnect(TenantId tenantId, DeviceId deviceId) { + onDeviceConnect(tenantId, deviceId, System.currentTimeMillis()); + } void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime); - void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId); + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long lastDisconnectTime); + + default void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) { + onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis()); + } + + void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long lastInactivityTime); void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 256a8ac89c..d1822a6ffe 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -36,8 +36,6 @@ public abstract class TbSubscription { private final TbSubscriptionType type; private final BiConsumer, T> updateProcessor; - protected final AtomicInteger sequence = new AtomicInteger(); - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 08a59973de..4e3d034207 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java index 7a7d176f81..35f2489f83 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.sync.ie.EntityExportData; @@ -65,13 +67,24 @@ public class NotificationRuleExportService ruleChains = triggerConfig.getRuleChains(); if (ruleChains != null) { triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet())); } break; + } + case EDGE_CONNECTION: { + EdgeConnectionNotificationRuleTriggerConfig triggerConfig = (EdgeConnectionNotificationRuleTriggerConfig) ruleTriggerConfig; + triggerConfig.setEdges(null); + break; + } + case EDGE_COMMUNICATION_FAILURE: { + EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig = (EdgeCommunicationFailureNotificationRuleTriggerConfig) ruleTriggerConfig; + triggerConfig.setEdges(null); + break; + } } NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java index 72f0fa219e..dd53e5b5bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java @@ -33,6 +33,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; @@ -86,7 +88,7 @@ public class NotificationRuleImportService extends BaseEntityImportService ruleChains = triggerConfig.getRuleChains(); if (ruleChains != null) { @@ -95,6 +97,17 @@ public class NotificationRuleImportService extends BaseEntityImportService void sendUpdate(String sessionId, int cmdId, T update) { + private void doSendUpdate(String sessionId, int cmdId, T update) { WsSessionMetaData md = wsSessionsMap.get(sessionId); if (md != null) { sendUpdate(md.getSessionRef(), cmdId, update); @@ -288,7 +288,7 @@ public class DefaultWebSocketService implements WebSocketService { try { msgEndpoint.close(md.getSessionRef(), status); } catch (IOException e) { - log.warn("[{}] Failed to send session close: {}", sessionId, e); + log.warn("[{}] Failed to send session close", sessionId, e); } } } @@ -439,7 +439,7 @@ public class DefaultWebSocketService implements WebSocketService { TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) + .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .queryTs(queryTs) @@ -449,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService { .updateProcessor((subscription, update) -> { subLock.lock(); try { - sendUpdate(subscription.getSessionId(), update); + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); } finally { subLock.unlock(); } @@ -545,7 +545,7 @@ public class DefaultWebSocketService implements WebSocketService { TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) + .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .queryTs(queryTs) @@ -554,7 +554,7 @@ public class DefaultWebSocketService implements WebSocketService { .updateProcessor((subscription, update) -> { subLock.lock(); try { - sendUpdate(subscription.getSessionId(), update); + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); } finally { subLock.unlock(); } @@ -643,13 +643,13 @@ public class DefaultWebSocketService implements WebSocketService { TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) + .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .updateProcessor((subscription, update) -> { subLock.lock(); try { - sendUpdate(subscription.getSessionId(), update); + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); } finally { subLock.unlock(); } @@ -698,13 +698,13 @@ public class DefaultWebSocketService implements WebSocketService { TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(cmd.getCmdId()) + .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .updateProcessor((subscription, update) -> { subLock.lock(); try { - sendUpdate(subscription.getSessionId(), update); + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); } finally { subLock.unlock(); } @@ -836,7 +836,7 @@ public class DefaultWebSocketService implements WebSocketService { try { msgEndpoint.sendPing(md.getSessionRef(), currentTime); } catch (IOException e) { - log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); + log.warn("[{}] Failed to send ping:", md.getSessionRef().getSessionId(), e); } })); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java index dea7c9813b..8f586f264e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java @@ -29,7 +29,7 @@ public interface WebSocketService { void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper); - void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update); + void sendUpdate(String sessionId, int cmdId, TelemetrySubscriptionUpdate update); void sendUpdate(String sessionId, CmdUpdate update); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index a5368e0f5e..f868a9d4f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -115,7 +115,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId()); - subscription.getUnreadCounter().set(unreadCount); + subscription.getTotalUnreadCounter().set(unreadCount); } @@ -196,20 +196,20 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) { log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); if (update.isCreated()) { - subscription.getUnreadCounter().incrementAndGet(); + subscription.getTotalUnreadCounter().incrementAndGet(); sendUpdate(subscription.getSessionId(), subscription.createUpdate()); } else if (update.isUpdated()) { if (update.getNewStatus() == NotificationStatus.READ) { if (update.isAllNotifications()) { fetchUnreadNotificationsCount(subscription); } else { - subscription.getUnreadCounter().decrementAndGet(); + subscription.getTotalUnreadCounter().decrementAndGet(); } sendUpdate(subscription.getSessionId(), subscription.createUpdate()); } } else if (update.isDeleted()) { if (update.getNotification().getStatus() != NotificationStatus.READ) { - subscription.getUnreadCounter().decrementAndGet(); + subscription.getTotalUnreadCounter().decrementAndGet(); sendUpdate(subscription.getSessionId(), subscription.createUpdate()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java new file mode 100644 index 0000000000..37ba69589b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/AbstractNotificationSubscription.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ws.notification.sub; + + +import lombok.Getter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.service.subscription.TbSubscription; +import org.thingsboard.server.service.subscription.TbSubscriptionType; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +@Getter +public abstract class AbstractNotificationSubscription extends TbSubscription { + + protected final AtomicInteger sequence = new AtomicInteger(); + protected final AtomicInteger totalUnreadCounter = new AtomicInteger(); + + public AbstractNotificationSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, TbSubscriptionType type, BiConsumer, T> updateProcessor) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, type, updateProcessor); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java index 145f69d6b6..1e812c3b31 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java @@ -27,9 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; @Getter -public class NotificationsCountSubscription extends TbSubscription { - - private final AtomicInteger unreadCounter = new AtomicInteger(); +public class NotificationsCountSubscription extends AbstractNotificationSubscription { @Builder public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, @@ -40,7 +38,7 @@ public class NotificationsCountSubscription extends TbSubscription { +public class NotificationsSubscription extends AbstractNotificationSubscription { private final Map latestUnreadNotifications = new HashMap<>(); private final int limit; - private final AtomicInteger totalUnreadCounter = new AtomicInteger(); @Builder public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 70a5146bb7..34c30859c2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -786,7 +786,7 @@ state: defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}" defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" # Interval for checking the device state after a specified period. Time in seconds # Controls whether we store the device 'active' flag in attributes (default) or telemetry. - # If you device to change this parameter, you should re-create the device info view as one of the following: + # If you decide to change this parameter, you should re-create the device info view as one of the following: # If 'persistToTelemetry' is changed from 'false' to 'true': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_ts_view;' # If 'persistToTelemetry' is changed from 'true' to 'false': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;' persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" @@ -794,6 +794,15 @@ state: # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data. # 0 means time-to-live mechanism is disabled. telemetryTtl: "${STATE_TELEMETRY_TTL:0}" + # Configuration properties for rule nodes related to device activity state + rule: + node: + # Device state rule node + deviceState: + # Defines the rate at which device connectivity events can be triggered. + # Comma-separated list of capacity:duration pairs that define bandwidth capacity and refill duration for token bucket rate limit algorithm. + # Refill is set to be greedy. Please refer to Bucket4j library documentation for more details. + rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}" # Tbel parameters tbel: @@ -1044,6 +1053,8 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" + # "" disables connection id support, 0 enables support but not for incoming traffic, any value greater than 0 set the connection id size in bytes + connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:6}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" @@ -1173,14 +1184,18 @@ transport: bind_port: "${SNMP_BIND_PORT:1620}" response_processing: # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices - parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" + parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:4}" # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" - # Batch size to request OID mappings from the device (useful when the device profile has multiple hundreds of OID mappings) + # Maximum size of a PDU (amount of OID mappings in a single SNMP request). The request will be split into multiple PDUs if mappings amount exceeds this number max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" + # Delay after sending each request chunk (in case the request was split into multiple PDUs due to max_request_oids) + request_chunk_delay_ms: "${SNMP_REQUEST_CHUNK_DELAY_MS:100}" response: # To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types) ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" + # Thread pool size for scheduler that executes device querying tasks + scheduler_thread_pool_size: "${SNMP_SCHEDULER_THREAD_POOL_SIZE:4}" stats: # Enable/Disable the collection of transport statistics enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 44302f08ba..314fe4c28c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -154,24 +154,6 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.reset(tbClusterService, auditLogService); } - protected void testNotifyManyEntityManyTimeMsgToEdgeServiceNever(HasName entity, HasName originator, - TenantId tenantId, CustomerId customerId, UserId userId, String userName, - ActionType actionType, int cntTime, Object... additionalInfo) { - EntityId entityId = createEntityId_NULL_UUID(entity); - EntityId originatorId = createEntityId_NULL_UUID(originator); - testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType); - ArgumentMatcher matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass()); - ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass()); - ArgumentMatcher matcherCustomerId = customerId == null ? - argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId); - ArgumentMatcher matcherUserId = userId == null ? - argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId); - testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, - extractMatcherAdditionalInfo(additionalInfo)); - testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); - Mockito.reset(tbClusterService, auditLogService); - } - protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(HasName entity, HasName originator, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, @@ -624,7 +606,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { private String entityClassToString(HasName entity) { String className = entity.getClass().toString() .substring(entity.getClass().toString().lastIndexOf(".") + 1); - List str = className.chars() + List str = className.chars() .mapToObj(x -> (Character.isUpperCase(x)) ? "_" + Character.toString(x) : Character.toString(x)) .collect(Collectors.toList()); return String.join("", str).toUpperCase(Locale.ENGLISH).substring(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 488bd4ed70..458bb3240e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -711,7 +711,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(30, TimeUnit.SECONDS)); + Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(TIMEOUT, TimeUnit.SECONDS)); Assert.assertEquals(JacksonUtil.newObjectNode().put(attrKey, attrValue), JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); @@ -798,7 +798,21 @@ public class DeviceEdgeTest extends AbstractEdgeTest { // clean up stored edge events edgeEventService.cleanupEvents(1); - // perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly + // edge is disconnected: perform rpc call - no edge event saved + doPostAsync( + "/api/rpc/oneway/" + device.getId().getId().toString(), + JacksonUtil.toString(createDefaultRpc()), + String.class, + status().isOk()); + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + PageData result = edgeEventService.findEdgeEvents(tenantId, tmpEdge.getId(), 0L, null, new TimePageLink(1)); + return result.getTotalElements() == 0; + }); + + // edge is connected: perform rpc call to verify edgeId in DeviceActorMessageProcessor updated properly + simulateEdgeActivation(tmpEdge); doPostAsync( "/api/rpc/oneway/" + device.getId().getId().toString(), JacksonUtil.toString(createDefaultRpc()), @@ -857,4 +871,23 @@ public class DeviceEdgeTest extends AbstractEdgeTest { return rpc; } + + private void simulateEdgeActivation(Edge edge) throws Exception { + ObjectNode attributes = JacksonUtil.newObjectNode(); + attributes.put("active", true); + doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); + Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); + if (activeAttrOpt.isEmpty()) { + return false; + } + Map activeAttr = activeAttrOpt.get(); + return "true".equals(activeAttr.get("value").toString()); + }); + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 4942dbdb9b..62d2c5eade 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -146,7 +146,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { } } - private void createRuleChainMetadata(RuleChain ruleChain) { + private RuleChainMetaData createRuleChainMetadata(RuleChain ruleChain) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChain.getId()); @@ -182,7 +182,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { ruleChainMetaData.addConnectionInfo(0, 2, "fail"); ruleChainMetaData.addConnectionInfo(1, 2, "success"); - doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + return doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); } @Test @@ -193,9 +193,10 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { ruleChain.setType(RuleChainType.EDGE); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doPost("/api/edge/" + edge.getUuidId() + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain); Assert.assertTrue(edgeImitator.waitForMessages()); // set new rule chain as root @@ -213,6 +214,22 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Assert.assertTrue(ruleChainMsg.isRoot()); Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); + // update metadata for root rule chain + edgeImitator.expectMessageAmount(1); + metaData.getNodes().forEach(n -> n.setDebugMode(true)); + doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); + Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); + ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); + ruleChainMsg = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); + Assert.assertNotNull(ruleChainMsg); + Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) || + UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType())); + Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); + Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName()); + Assert.assertTrue(ruleChainMsg.isRoot()); + // revert root rule chain edgeImitator.expectMessageAmount(1); doPost("/api/edge/" + edge.getUuidId() diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index f5e018ed3a..916e5790e9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient; +import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; @@ -72,8 +73,6 @@ import java.util.stream.Collectors; @Slf4j public class EdgeImitator { - public static final int TIMEOUT_IN_SECONDS = 30; - private String routingKey; private String routingSecret; @@ -344,7 +343,7 @@ public class EdgeImitator { } public boolean waitForMessages() throws InterruptedException { - return waitForMessages(TIMEOUT_IN_SECONDS); + return waitForMessages(AbstractWebTest.TIMEOUT); } public boolean waitForMessages(int timeoutInSeconds) throws InterruptedException { @@ -359,7 +358,7 @@ public class EdgeImitator { } public boolean waitForResponses() throws InterruptedException { - return responsesLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + return responsesLatch.await(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS); } public void expectResponsesAmount(int messageAmount) { diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 14d017a7a4..7f8515f50b 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -315,7 +315,7 @@ public class HashPartitionServiceTest { .setPartitions(isolatedQueue.getPartitions()) .build(); - partitionService_common.updateQueue(queueUpdateMsg); + partitionService_common.updateQueues(List.of(queueUpdateMsg)); partitionService_common.recalculatePartitions(commonRuleEngine, List.of(dedicatedRuleEngine)); // expecting event about no partitions for isolated queue key verifyPartitionChangeEvent(event -> { @@ -323,7 +323,7 @@ public class HashPartitionServiceTest { return event.getPartitionsMap().get(queueKey).isEmpty(); }); - partitionService_dedicated.updateQueue(queueUpdateMsg); + partitionService_dedicated.updateQueues(List.of(queueUpdateMsg)); partitionService_dedicated.recalculatePartitions(dedicatedRuleEngine, List.of(commonRuleEngine)); verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); @@ -342,7 +342,7 @@ public class HashPartitionServiceTest { .setQueueIdLSB(isolatedQueue.getUuidId().getLeastSignificantBits()) .setQueueName(isolatedQueue.getName()) .build(); - partitionService_dedicated.removeQueue(queueDeleteMsg); + partitionService_dedicated.removeQueues(List.of(queueDeleteMsg)); verifyPartitionChangeEvent(event -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId); return event.getPartitionsMap().get(queueKey).isEmpty(); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java index ce6ca753b3..d6bfbf1036 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edge.rpc.constructor; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; @@ -61,7 +60,7 @@ public class RuleChainMsgConstructorTest { } @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() throws JsonProcessingException { + public void testConstructRuleChainMetadataUpdatedMsg_V_3_4_0() { RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); @@ -80,7 +79,7 @@ public class RuleChainMsgConstructorTest { } @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() throws JsonProcessingException { + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() { RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData( ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); @@ -120,7 +119,7 @@ public class RuleChainMsgConstructorTest { } @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() throws JsonProcessingException { + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() { RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = @@ -161,7 +160,7 @@ public class RuleChainMsgConstructorTest { } @Test - public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() throws JsonProcessingException { + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() { // same rule chain metadata, but different order of rule nodes RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); @@ -254,7 +253,7 @@ public class RuleChainMsgConstructorTest { return result; } - private List createRuleNodes(RuleChainId ruleChainId) throws JsonProcessingException { + private List createRuleNodes(RuleChainId ruleChainId) { List result = new ArrayList<>(); result.add(getOutputNode(ruleChainId)); result.add(getAcknowledgeNode(ruleChainId)); @@ -301,7 +300,7 @@ public class RuleChainMsgConstructorTest { return result; } - private List createRuleNodesInDifferentOrder(RuleChainId ruleChainId) throws JsonProcessingException { + private List createRuleNodesInDifferentOrder(RuleChainId ruleChainId) { List result = new ArrayList<>(); result.add(getPushToAnalyticsNode(ruleChainId)); result.add(getPushToCloudNode(ruleChainId)); @@ -319,99 +318,99 @@ public class RuleChainMsgConstructorTest { } - private RuleNode getOutputNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getOutputNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode", "Output node", - JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); + JacksonUtil.toJsonNode("{\"version\":0}"), + JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); } - private RuleNode getCheckpointNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getCheckpointNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.flow.TbCheckpointNode", "Checkpoint node", - JacksonUtil.OBJECT_MAPPER.readTree("{\"queueName\":\"HighPriority\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); + JacksonUtil.toJsonNode("{\"queueName\":\"HighPriority\"}"), + JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); } - private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "Save Timeseries", - JacksonUtil.OBJECT_MAPPER.readTree("{\"defaultTTL\":0}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":823,\"layoutY\":157}")); + JacksonUtil.toJsonNode("{\"defaultTTL\":0}"), + JacksonUtil.toJsonNode("{\"layoutX\":823,\"layoutY\":157}")); } - private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "Message Type Switch", - JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":347,\"layoutY\":149}")); + JacksonUtil.toJsonNode("{\"version\":0}"), + JacksonUtil.toJsonNode("{\"layoutX\":347,\"layoutY\":149}")); } - private RuleNode getLogOtherNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getLogOtherNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.action.TbLogNode", "Log Other", - JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":378}")); + JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), + JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":378}")); } - private RuleNode getPushToCloudNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getPushToCloudNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "Push to cloud", - JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"SERVER_SCOPE\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":1129,\"layoutY\":52}")); + JacksonUtil.toJsonNode("{\"scope\":\"SERVER_SCOPE\"}"), + JacksonUtil.toJsonNode("{\"layoutX\":1129,\"layoutY\":52}")); } - private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.flow.TbAckNode", "Acknowledge node", - JacksonUtil.OBJECT_MAPPER.readTree("{\"version\":0}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); + JacksonUtil.toJsonNode("{\"version\":0}"), + JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); } - private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "Device Profile Node", - JacksonUtil.OBJECT_MAPPER.readTree("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); + JacksonUtil.toJsonNode("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), + JacksonUtil.toJsonNode("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); } - private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "Save Client Attributes", - JacksonUtil.OBJECT_MAPPER.readTree("{\"scope\":\"CLIENT_SCOPE\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":52}")); + JacksonUtil.toJsonNode("{\"scope\":\"CLIENT_SCOPE\"}"), + JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":52}")); } - private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.action.TbLogNode", "Log RPC from Device", - JacksonUtil.OBJECT_MAPPER.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":825,\"layoutY\":266}")); + JacksonUtil.toJsonNode("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), + JacksonUtil.toJsonNode("{\"layoutX\":825,\"layoutY\":266}")); } - private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "RPC Call Request", - JacksonUtil.OBJECT_MAPPER.readTree("{\"timeoutInSeconds\":60}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"layoutX\":824,\"layoutY\":466}")); + JacksonUtil.toJsonNode("{\"timeoutInSeconds\":60}"), + JacksonUtil.toJsonNode("{\"layoutX\":824,\"layoutY\":466}")); } - private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) throws JsonProcessingException { + private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) { return createRuleNode(ruleChainId, "org.thingsboard.rule.engine.flow.TbRuleChainInputNode", "Push to Analytics", - JacksonUtil.OBJECT_MAPPER.readTree("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), - JacksonUtil.OBJECT_MAPPER.readTree("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); + JacksonUtil.toJsonNode("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), + JacksonUtil.toJsonNode("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); } -} \ No newline at end of file +} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index 9c45977a7c..835b39caf9 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -55,6 +55,7 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -209,6 +210,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected AttributesService attributesService; + @MockBean + protected TimeseriesService timeseriesService; + @MockBean protected TbClusterService tbClusterService; diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java index 9917e837b9..2570b80f9f 100644 --- a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java @@ -20,6 +20,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.cache.limits.DefaultRateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -28,9 +30,7 @@ import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.util.limits.DefaultRateLimitService; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.dao.tenant.DefaultTbTenantProfileCache; import java.util.List; import java.util.UUID; @@ -46,12 +46,12 @@ import static org.mockito.Mockito.when; public class RateLimitServiceTest { private RateLimitService rateLimitService; - private TbTenantProfileCache tenantProfileCache; + private DefaultTbTenantProfileCache tenantProfileCache; private TenantId tenantId; @Before public void beforeEach() { - tenantProfileCache = Mockito.mock(TbTenantProfileCache.class); + tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class); rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100); tenantId = new TenantId(UUID.randomUUID()); } @@ -69,6 +69,8 @@ public class RateLimitServiceTest { profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); + profileConfiguration.setEdgeEventRateLimits(rateLimit); + profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit); updateTenantProfileConfiguration(profileConfiguration); for (LimitedApi limitedApi : List.of( @@ -76,7 +78,9 @@ public class RateLimitServiceTest { LimitedApi.ENTITY_IMPORT, LimitedApi.NOTIFICATION_REQUESTS, LimitedApi.REST_REQUESTS_PER_CUSTOMER, - LimitedApi.CASSANDRA_QUERIES + LimitedApi.CASSANDRA_QUERIES, + LimitedApi.EDGE_EVENTS, + LimitedApi.EDGE_EVENTS_PER_EDGE )) { testRateLimits(limitedApi, max, tenantId); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index ed4a1c613a..1e95f9add3 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -93,7 +93,7 @@ import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService; import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache; import org.thingsboard.server.service.state.DeviceStateService; diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 6efb171fb4..91d4f3ef53 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -41,6 +41,7 @@ import org.thingsboard.server.service.gateway_device.GatewayNotificationsService import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -95,7 +96,7 @@ public class DefaultTbClusterServiceTest { when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer); - clusterService.onQueueChange(createTestQueue()); + clusterService.onQueuesUpdate(List.of(createTestQueue())); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH); verify(topicService, never()).getNotificationsTopic(eq(ServiceType.TB_CORE), any()); @@ -120,7 +121,7 @@ public class DefaultTbClusterServiceTest { when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbQueueProducer); - clusterService.onQueueChange(createTestQueue()); + clusterService.onQueuesUpdate(List.of(createTestQueue())); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2); @@ -148,7 +149,7 @@ public class DefaultTbClusterServiceTest { when(producerProvider.getRuleEngineNotificationsMsgProducer()).thenReturn(tbREQueueProducer); when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer); - clusterService.onQueueChange(createTestQueue()); + clusterService.onQueuesUpdate(List.of(createTestQueue())); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, MONOLITH); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_TRANSPORT, TRANSPORT); @@ -194,7 +195,7 @@ public class DefaultTbClusterServiceTest { when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer); when(producerProvider.getTransportNotificationsMsgProducer()).thenReturn(tbTransportQueueProducer); - clusterService.onQueueChange(createTestQueue()); + clusterService.onQueuesUpdate(List.of(createTestQueue())); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith1); verify(topicService, times(1)).getNotificationsTopic(ServiceType.TB_RULE_ENGINE, monolith2); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java new file mode 100644 index 0000000000..621d4c5a91 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java @@ -0,0 +1,532 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.state.DeviceStateService; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class DefaultTbCoreConsumerServiceTest { + + @Mock + private DeviceStateService stateServiceMock; + @Mock + private TbCoreConsumerStats statsMock; + + @Mock + private TbCallback tbCallbackMock; + + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); + private final long time = System.currentTimeMillis(); + + private ListeningExecutorService executor; + + @Mock + private DefaultTbCoreConsumerService defaultTbCoreConsumerServiceMock; + + @BeforeEach + public void setup() { + executor = MoreExecutors.newDirectExecutorService(); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stateService", stateServiceMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "deviceActivityEventsExecutor", executor); + } + + @AfterEach + public void cleanup() { + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(10L, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + @Test + public void givenProcessingSuccess_whenForwardingDeviceStateMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onQueueMsg(stateMsg, tbCallbackMock); + } + + @Test + public void givenStatsEnabled_whenForwardingDeviceStateMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(stateMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingDeviceStateMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var stateMsg = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setAdded(true) + .setUpdated(false) + .setDeleted(false) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(stateMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(stateMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(stateMsg); + } + + @Test + public void givenProcessingSuccess_whenForwardingConnectMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var connectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onDeviceConnect(tenantId, deviceId, time); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + @Test + public void givenProcessingFailure_whenForwardingConnectMsgToStateService_thenOnFailureCallbackIsCalled() { + // GIVEN + var connectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock); + + var runtimeException = new RuntimeException("Something bad happened!"); + doThrow(runtimeException).when(stateServiceMock).onDeviceConnect(tenantId, deviceId, time); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock); + + // THEN + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(runtimeException); + } + + @Test + public void givenStatsEnabled_whenForwardingConnectMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var connectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(connectMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingConnectMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var connectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastConnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(connectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(connectMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(connectMsg); + } + + @Test + public void givenProcessingSuccess_whenForwardingActivityMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var activityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onDeviceActivity(tenantId, deviceId, time); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + @Test + public void givenProcessingFailure_whenForwardingActivityMsgToStateService_thenOnFailureCallbackIsCalled() { + // GIVEN + var activityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock); + + var runtimeException = new RuntimeException("Something bad happened!"); + doThrow(runtimeException).when(stateServiceMock).onDeviceActivity(tenantId, deviceId, time); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock); + + // THEN + then(tbCallbackMock).should(never()).onSuccess(); + + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(tbCallbackMock).should().onFailure(exceptionCaptor.capture()); + assertThat(exceptionCaptor.getValue()) + .isInstanceOf(RuntimeException.class) + .hasMessage("Failed to update device activity for device [" + deviceId.getId() + "]!") + .hasCause(runtimeException); + } + + @Test + public void givenStatsEnabled_whenForwardingActivityMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var activityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(activityMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingActivityMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var activityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastActivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(activityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(activityMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(activityMsg); + } + + @Test + public void givenProcessingSuccess_whenForwardingDisconnectMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onDeviceDisconnect(tenantId, deviceId, time); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + @Test + public void givenProcessingFailure_whenForwardingDisconnectMsgToStateService_thenOnFailureCallbackIsCalled() { + // GIVEN + var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock); + + var runtimeException = new RuntimeException("Something bad happened!"); + doThrow(runtimeException).when(stateServiceMock).onDeviceDisconnect(tenantId, deviceId, time); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock); + + // THEN + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(runtimeException); + } + + @Test + public void givenStatsEnabled_whenForwardingDisconnectMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(disconnectMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingDisconnectMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var disconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastDisconnectTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(disconnectMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(disconnectMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(disconnectMsg); + } + + @Test + public void givenProcessingSuccess_whenForwardingInactivityMsgToStateService_thenOnSuccessCallbackIsCalled() { + // GIVEN + var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock); + + // THEN + then(stateServiceMock).should().onDeviceInactivity(tenantId, deviceId, time); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + @Test + public void givenProcessingFailure_whenForwardingInactivityMsgToStateService_thenOnFailureCallbackIsCalled() { + // GIVEN + var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock); + + var runtimeException = new RuntimeException("Something bad happened!"); + doThrow(runtimeException).when(stateServiceMock).onDeviceInactivity(tenantId, deviceId, time); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock); + + // THEN + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(runtimeException); + } + + @Test + public void givenStatsEnabled_whenForwardingInactivityMsgToStateService_thenStatsAreRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", true); + + var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock); + + // THEN + then(statsMock).should().log(inactivityMsg); + } + + @Test + public void givenStatsDisabled_whenForwardingInactivityMsgToStateService_thenStatsAreNotRecorded() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "stats", statsMock); + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "statsEnabled", false); + + var inactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .setLastInactivityTime(time) + .build(); + + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToStateService(inactivityMsg, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToStateService(inactivityMsg, tbCallbackMock); + + // THEN + then(statsMock).should(never()).log(inactivityMsg); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 83c1f138f2..8db79a8553 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -21,11 +21,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,19 +56,26 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @@ -74,6 +83,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACT import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_ALARM_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.INACTIVITY_TIMEOUT; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_ACTIVITY_TIME; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME; +import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; @ExtendWith(MockitoExtension.class) public class DefaultDeviceStateServiceTest { @@ -116,21 +127,366 @@ public class DefaultDeviceStateServiceTest { tpi = TopicPartitionInfo.builder().myPartition(true).build(); } + @Test + public void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() { + // GIVEN + doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceConnect(tenantId, deviceId, System.currentTimeMillis()); + + // THEN + then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + then(service).should(never()).getOrFetchDeviceStateData(deviceId); + then(service).should(never()).checkAndUpdateState(eq(deviceId), any()); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) + public void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceConnect(tenantId, deviceId, negativeLastConnectTime); + + // THEN + then(service).should(never()).getOrFetchDeviceStateData(deviceId); + then(service).should(never()).checkAndUpdateState(eq(deviceId), any()); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @MethodSource("provideOutdatedTimestamps") + public void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().lastConnectTime(currentLastConnectTime).build()) + .build(); + service.deviceStates.put(deviceId, deviceStateData); + + // WHEN + service.onDeviceConnect(tenantId, deviceId, outdatedLastConnectTime); + + // THEN + then(service).should(never()).checkAndUpdateState(eq(deviceId), any()); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @Test + public void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() { + // GIVEN + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + service.deviceStates.put(deviceId, deviceStateData); + long lastConnectTime = System.currentTimeMillis(); + + // WHEN + service.onDeviceConnect(tenantId, deviceId, lastConnectTime); + + // THEN + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), eq(LAST_CONNECT_TIME), eq(lastConnectTime), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.CONNECT_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + } + + @Test + public void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() { + // GIVEN + doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceDisconnect(tenantId, deviceId, System.currentTimeMillis()); + + // THEN + then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + then(service).should(never()).getOrFetchDeviceStateData(deviceId); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) + public void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceDisconnect(tenantId, deviceId, negativeLastDisconnectTime); + + // THEN + then(service).should(never()).getOrFetchDeviceStateData(deviceId); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @MethodSource("provideOutdatedTimestamps") + public void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().lastDisconnectTime(currentLastDisconnectTime).build()) + .build(); + service.deviceStates.put(deviceId, deviceStateData); + + // WHEN + service.onDeviceDisconnect(tenantId, deviceId, outdatedLastDisconnectTime); + + // THEN + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @Test + public void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() { + // GIVEN + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + service.deviceStates.put(deviceId, deviceStateData); + long lastDisconnectTime = System.currentTimeMillis(); + + // WHEN + service.onDeviceDisconnect(tenantId, deviceId, lastDisconnectTime); + + // THEN + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(LAST_DISCONNECT_TIME), eq(lastDisconnectTime), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should().pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.DISCONNECT_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + } + + @Test + public void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() { + // GIVEN + doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId, System.currentTimeMillis()); + + // THEN + then(service).should().cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + then(service).should(never()).fetchDeviceStateDataUsingSeparateRequests(deviceId); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) + public void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId, negativeLastInactivityTime); + + // THEN + then(service).should(never()).getOrFetchDeviceStateData(deviceId); + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @MethodSource("provideOutdatedTimestamps") + public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( + long outdatedLastInactivityTime, long currentLastInactivityTime + ) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().lastInactivityAlarmTime(currentLastInactivityTime).build()) + .build(); + service.deviceStates.put(deviceId, deviceStateData); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId, outdatedLastInactivityTime); + + // THEN + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @MethodSource("provideOutdatedTimestamps") + public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( + long outdatedLastInactivityTime, long currentLastActivityTime + ) { + // GIVEN + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().lastActivityTime(currentLastActivityTime).build()) + .build(); + service.deviceStates.put(deviceId, deviceStateData); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId, outdatedLastInactivityTime); + + // THEN + then(clusterService).shouldHaveNoInteractions(); + then(notificationRuleProcessor).shouldHaveNoInteractions(); + then(telemetrySubscriptionService).shouldHaveNoInteractions(); + } + + private static Stream provideOutdatedTimestamps() { + return Stream.of( + Arguments.of(0, 0), + Arguments.of(0, 100), + Arguments.of(50, 100), + Arguments.of(99, 100), + Arguments.of(100, 100) + ); + } + + @Test + public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { + // GIVEN + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); + + service.deviceStates.put(deviceId, deviceStateData); + long lastInactivityTime = System.currentTimeMillis(); + + // WHEN + service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime); + + // THEN + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(INACTIVITY_ALARM_TIME), eq(lastInactivityTime), any() + ); + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(ACTIVITY_STATE), eq(false), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should() + .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + + var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + var actualNotification = notificationCaptor.getValue(); + assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); + assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); + assertThat(actualNotification.isActive()).isFalse(); + } + + @Test + public void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() { + // GIVEN + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(DeviceState.builder().build()) + .metaData(new TbMsgMetaData()) + .build(); + + given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); + + // WHEN + service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); + + // THEN + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(INACTIVITY_ALARM_TIME), anyLong(), any() + ); + then(telemetrySubscriptionService).should().saveAttrAndNotify( + eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(DataConstants.SERVER_SCOPE), + eq(ACTIVITY_STATE), eq(false), any() + ); + + var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(clusterService).should() + .pushMsgToRuleEngine(eq(tenantId), eq(deviceId), msgCaptor.capture(), any()); + var actualMsg = msgCaptor.getValue(); + assertThat(actualMsg.getType()).isEqualTo(TbMsgType.INACTIVITY_EVENT.name()); + assertThat(actualMsg.getOriginator()).isEqualTo(deviceId); + + var notificationCaptor = ArgumentCaptor.forClass(DeviceActivityTrigger.class); + then(notificationRuleProcessor).should().process(notificationCaptor.capture()); + var actualNotification = notificationCaptor.getValue(); + assertThat(actualNotification.getTenantId()).isEqualTo(tenantId); + assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); + assertThat(actualNotification.isActive()).isFalse(); + } + @Test public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { service.deviceStates.put(deviceId, deviceStateDataMock); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); assertThat(deviceStateData).isEqualTo(deviceStateDataMock); - verify(service, never()).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + verify(service, never()).fetchDeviceStateDataUsingSeparateRequests(deviceId); } @Test public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { service.deviceStates.clear(); - willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); assertThat(deviceStateData).isEqualTo(deviceStateDataMock); - verify(service, times(1)).fetchDeviceStateDataUsingEntityDataQuery(deviceId); + verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); } @Test @@ -356,7 +712,7 @@ public class DefaultDeviceStateServiceTest { } private void activityVerify(boolean isActive) { - verify(telemetrySubscriptionService, times(1)).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any()); + verify(telemetrySubscriptionService).saveAttrAndNotify(any(), eq(deviceId), any(), eq(ACTIVITY_STATE), eq(isActive), any()); } @Test @@ -435,28 +791,28 @@ public class DefaultDeviceStateServiceTest { private static Stream provideParametersForUpdateActivityState() { return Stream.of( - Arguments.of(true, 100, 120, 80, 80, false, false), + Arguments.of(true, 100, 120, 80, 80, false, false), - Arguments.of(true, 100, 120, 100, 100, false, false), + Arguments.of(true, 100, 120, 100, 100, false, false), Arguments.of(false, 100, 120, 110, 110, false, true), - Arguments.of(true, 100, 100, 80, 80, false, false), + Arguments.of(true, 100, 100, 80, 80, false, false), - Arguments.of(true, 100, 100, 100, 100, false, false), + Arguments.of(true, 100, 100, 100, 100, false, false), - Arguments.of(false, 100, 100, 110, 0, true, true), + Arguments.of(false, 100, 100, 110, 0, true, true), - Arguments.of(false, 100, 110, 110, 0, true, true), + Arguments.of(false, 100, 110, 110, 0, true, true), - Arguments.of(false, 100, 110, 120, 0, true, true), + Arguments.of(false, 100, 110, 120, 0, true, true), - Arguments.of(true, 0, 0, 0, 0, false, false), + Arguments.of(true, 0, 0, 0, 0, false, false), - Arguments.of(false, 0, 0, 0, 0, true, true) + Arguments.of(false, 0, 0, 0, 0, true, true) ); } @@ -679,4 +1035,40 @@ public class DefaultDeviceStateServiceTest { ); } + @Test + public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { + doAnswer(invocation -> { + Thread.sleep(100); + return deviceStateDataMock; + }).when(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); + + int numberOfThreads = 10; + var allThreadsReadyLatch = new CountDownLatch(numberOfThreads); + + ExecutorService executor = null; + try { + executor = Executors.newFixedThreadPool(numberOfThreads); + for (int i = 0; i < numberOfThreads; i++) { + executor.submit(() -> { + allThreadsReadyLatch.countDown(); + try { + allThreadsReadyLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + service.getOrFetchDeviceStateData(deviceId); + }); + } + + executor.shutdown(); + await().atMost(10, TimeUnit.SECONDS).until(executor::isTerminated); + } finally { + if (executor != null) { + executor.shutdownNow(); + } + } + + then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java new file mode 100644 index 0000000000..c97305c170 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultRuleEngineDeviceStateManagerTest.java @@ -0,0 +1,266 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class DefaultRuleEngineDeviceStateManagerTest { + + @Mock + private static DeviceStateService deviceStateServiceMock; + @Mock + private static TbCallback tbCallbackMock; + @Mock + private static TbClusterService clusterServiceMock; + @Mock + private static TbQueueMsgMetadata metadataMock; + + @Mock + private TbServiceInfoProvider serviceInfoProviderMock; + @Mock + private PartitionService partitionServiceMock; + + @Captor + private static ArgumentCaptor queueCallbackCaptor; + + private static DefaultRuleEngineDeviceStateManager deviceStateManager; + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("57ab2e6c-bc4c-11ee-a506-0242ac120002")); + private static final DeviceId DEVICE_ID = DeviceId.fromString("74a9053e-bc4c-11ee-a506-0242ac120002"); + private static final long EVENT_TS = System.currentTimeMillis(); + private static final RuntimeException RUNTIME_EXCEPTION = new RuntimeException("Something bad happened!"); + private static final TopicPartitionInfo MY_TPI = TopicPartitionInfo.builder().myPartition(true).build(); + private static final TopicPartitionInfo EXTERNAL_TPI = TopicPartitionInfo.builder().myPartition(false).build(); + + @BeforeEach + public void setup() { + deviceStateManager = new DefaultRuleEngineDeviceStateManager(serviceInfoProviderMock, partitionServiceMock, Optional.of(deviceStateServiceMock), clusterServiceMock); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to local service and event processed has succeeded, " + + "when onDeviceX() is called, then should route event to local service and call onSuccess() callback.") + @MethodSource + public void givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback(Runnable onDeviceAction, Runnable actionVerification) { + // GIVEN + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI); + + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + then(clusterServiceMock).shouldHaveNoInteractions(); + then(tbCallbackMock).should().onSuccess(); + then(tbCallbackMock).should(never()).onFailure(any()); + } + + private static Stream givenRoutedToLocalAndProcessingSuccess_whenOnDeviceAction_thenShouldCallLocalServiceAndSuccessCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to local service and event processed has failed, " + + "when onDeviceX() is called, then should route event to local service and call onFailure() callback.") + @MethodSource + public void givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback( + Runnable exceptionThrowSetup, Runnable onDeviceAction, Runnable actionVerification + ) { + // GIVEN + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(true); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(MY_TPI); + + exceptionThrowSetup.run(); + + // WHEN + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + then(clusterServiceMock).shouldHaveNoInteractions(); + then(tbCallbackMock).should(never()).onSuccess(); + then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); + } + + private static Stream givenRoutedToLocalAndProcessingFailure_whenOnDeviceAction_thenShouldCallLocalServiceAndFailureCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS) + ), + Arguments.of( + (Runnable) () -> doThrow(RUNTIME_EXCEPTION).when(deviceStateServiceMock).onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS), + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> then(deviceStateServiceMock).should().onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS) + ) + ); + } + + @ParameterizedTest + @DisplayName("Given event should be routed to external service, " + + "when onDeviceX() is called, then should send correct queue message to external service with correct callback object.") + @MethodSource + public void givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback(Runnable onDeviceAction, Runnable actionVerification) { + // WHEN + ReflectionTestUtils.setField(deviceStateManager, "deviceStateService", Optional.empty()); + given(serviceInfoProviderMock.isService(ServiceType.TB_CORE)).willReturn(false); + given(partitionServiceMock.resolve(ServiceType.TB_CORE, TENANT_ID, DEVICE_ID)).willReturn(EXTERNAL_TPI); + + onDeviceAction.run(); + + // THEN + actionVerification.run(); + + TbQueueCallback callback = queueCallbackCaptor.getValue(); + callback.onSuccess(metadataMock); + then(tbCallbackMock).should().onSuccess(); + callback.onFailure(RUNTIME_EXCEPTION); + then(tbCallbackMock).should().onFailure(RUNTIME_EXCEPTION); + } + + private static Stream givenRoutedToExternal_whenOnDeviceAction_thenShouldSendQueueMsgToExternalServiceWithCorrectCallback() { + return Stream.of( + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceConnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceConnectMsg = TransportProtos.DeviceConnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastConnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceConnectMsg(deviceConnectMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceActivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastActivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceActivityMsg(deviceActivityMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceDisconnect(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceDisconnectMsg = TransportProtos.DeviceDisconnectProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastDisconnectTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceDisconnectMsg(deviceDisconnectMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ), + Arguments.of( + (Runnable) () -> deviceStateManager.onDeviceInactivity(TENANT_ID, DEVICE_ID, EVENT_TS, tbCallbackMock), + (Runnable) () -> { + var deviceInactivityMsg = TransportProtos.DeviceInactivityProto.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setDeviceIdMSB(DEVICE_ID.getId().getMostSignificantBits()) + .setDeviceIdLSB(DEVICE_ID.getId().getLeastSignificantBits()) + .setLastInactivityTime(EVENT_TS) + .build(); + var toCoreMsg = TransportProtos.ToCoreMsg.newBuilder() + .setDeviceInactivityMsg(deviceInactivityMsg) + .build(); + then(clusterServiceMock).should().pushMsgToCore(eq(EXTERNAL_TPI), any(UUID.class), eq(toCoreMsg), queueCallbackCaptor.capture()); + } + ) + ); + } + +} diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 6b9544e761..184aa615ba 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -40,6 +40,10 @@ org.thingsboard.common data + + org.thingsboard.common + message + org.springframework.boot spring-boot-autoconfigure diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java similarity index 92% rename from dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java rename to common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java index bcd560a6f9..f3530e99d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.util.limits; +package org.thingsboard.server.cache.limits; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -28,25 +28,25 @@ import org.thingsboard.server.common.data.exception.TenantProfileNotFoundExcepti import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.concurrent.TimeUnit; +@Lazy @Service @Slf4j public class DefaultRateLimitService implements RateLimitService { - private final TbTenantProfileCache tenantProfileCache; + private final TenantProfileProvider tenantProfileProvider; private final NotificationRuleProcessor notificationRuleProcessor; - public DefaultRateLimitService(TbTenantProfileCache tenantProfileCache, + public DefaultRateLimitService(TenantProfileProvider tenantProfileProvider, @Lazy NotificationRuleProcessor notificationRuleProcessor, @Value("${cache.rateLimits.timeToLiveInMinutes:120}") int rateLimitsTtl, @Value("${cache.rateLimits.maxSize:200000}") int rateLimitsCacheMaxSize) { - this.tenantProfileCache = tenantProfileCache; + this.tenantProfileProvider = tenantProfileProvider; this.notificationRuleProcessor = notificationRuleProcessor; this.rateLimits = Caffeine.newBuilder() .expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES) @@ -66,7 +66,7 @@ public class DefaultRateLimitService implements RateLimitService { if (tenantId.isSysTenantId()) { return true; } - TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + TenantProfile tenantProfile = tenantProfileProvider.get(tenantId); if (tenantProfile == null) { throw new TenantProfileNotFoundException(tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java similarity index 95% rename from dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java rename to common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java index e24383bb62..84c22c514b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.util.limits; +package org.thingsboard.server.cache.limits; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/TenantProfileProvider.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/TenantProfileProvider.java new file mode 100644 index 0000000000..15243d12c8 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/TenantProfileProvider.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.limits; + +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TenantProfileProvider { + + TenantProfile get(TenantId tenantId); + +} diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java index a4502da4b1..11b60c86bd 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueClusterService.java @@ -17,8 +17,12 @@ package org.thingsboard.server.queue; import org.thingsboard.server.common.data.queue.Queue; +import java.util.List; + public interface TbQueueClusterService { - void onQueueChange(Queue queue); - void onQueueDelete(Queue queue); + void onQueuesUpdate(List queues); + + void onQueuesDelete(List queues); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index 15e55d8713..d6d5551ccf 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -91,4 +91,6 @@ public interface EdgeService extends EntityDaoService { PageData findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId, String tbRuleChainInputNodeClassName); + + ListenableFuture isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String activityState); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 5f743f626a..980eb880a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -31,6 +31,8 @@ public enum LimitedApi { REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false), WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true), CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true), + EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true), + EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false), PASSWORD_RESET(false, true), TWO_FA_VERIFICATION_CODE_SEND(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java index 8eb4451e3b..cfc91d8fca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java @@ -28,6 +28,7 @@ public enum NotificationType { ENTITIES_LIMIT, API_USAGE_LIMIT, RULE_NODE, - RATE_LIMITS - + RATE_LIMITS, + EDGE_CONNECTION, + EDGE_COMMUNICATION_FAILURE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java new file mode 100644 index 0000000000..6d0f2ae5f5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.info; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Map; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EdgeCommunicationFailureNotificationInfo implements RuleOriginatedNotificationInfo { + + private TenantId tenantId; + private CustomerId customerId; + private EdgeId edgeId; + private String edgeName; + private String failureMsg; + + @Override + public Map getTemplateData() { + return mapOf( + "edgeId", edgeId.toString(), + "edgeName", edgeName, + "failureMsg", failureMsg + ); + } + + @Override + public TenantId getAffectedTenantId() { + return tenantId; + } + + @Override + public CustomerId getAffectedCustomerId() { + return customerId; + } + + @Override + public EntityId getStateEntityId() { + return edgeId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java new file mode 100644 index 0000000000..62b1370566 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.info; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Map; + +import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EdgeConnectionNotificationInfo implements RuleOriginatedNotificationInfo { + + private String eventType; + private TenantId tenantId; + private CustomerId customerId; + private EdgeId edgeId; + private String edgeName; + + @Override + public Map getTemplateData() { + return mapOf( + "eventType", eventType, + "edgeId", edgeId.toString(), + "edgeName", edgeName + ); + } + + @Override + public TenantId getAffectedTenantId() { + return tenantId; + } + + @Override + public CustomerId getAffectedCustomerId() { + return customerId; + } + + @Override + public EntityId getStateEntityId() { + return edgeId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java new file mode 100644 index 0000000000..6212c22a0d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.rule.trigger; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +import java.util.concurrent.TimeUnit; + +@Data +@Builder +public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger { + + private final TenantId tenantId; + private final CustomerId customerId; + private final EdgeId edgeId; + private final String edgeName; + private final String failureMsg; + private final String error; + + @Override + public boolean deduplicate() { + return true; + } + + @Override + public String getDeduplicationKey() { + return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), error); + } + + @Override + public long getDefaultDeduplicationDuration() { + return TimeUnit.MINUTES.toMillis(30); + } + + @Override + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE; + } + + @Override + public EntityId getOriginatorEntityId() { + return edgeId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java new file mode 100644 index 0000000000..766338dba6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.rule.trigger; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +import java.util.concurrent.TimeUnit; + +@Data +@Builder +public class EdgeConnectionTrigger implements NotificationRuleTrigger { + + private final TenantId tenantId; + private final CustomerId customerId; + private final EdgeId edgeId; + private final boolean connected; + private final String edgeName; + + @Override + public boolean deduplicate() { + return true; + } + + @Override + public String getDeduplicationKey() { + return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), String.valueOf(connected)); + } + + @Override + public long getDefaultDeduplicationDuration() { + return TimeUnit.MINUTES.toMillis(1); + } + + @Override + public NotificationRuleTriggerType getType() { + return NotificationRuleTriggerType.EDGE_CONNECTION; + } + + @Override + public EntityId getOriginatorEntityId() { + return edgeId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java new file mode 100644 index 0000000000..595b0fa7d5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.rule.trigger.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EdgeCommunicationFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + + private Set edges; // if empty - all edges + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java new file mode 100644 index 0000000000..8c0905cc59 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.rule.trigger.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class EdgeConnectionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + + private Set edges; // if empty - all edges + private Set notifyOn; + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_CONNECTION; + } + + public enum EdgeConnectivityEvent { + CONNECTED, DISCONNECTED + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java index 5ffb8a5fe1..15a5e59255 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java @@ -36,6 +36,8 @@ import java.io.Serializable; @Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"), @Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"), @Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"), + @Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"), + @Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"), }) public interface NotificationRuleTriggerConfig extends Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java index 6eff7a8114..8469fac752 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java @@ -26,6 +26,8 @@ public enum NotificationRuleTriggerType { ALARM_ASSIGNMENT, DEVICE_ACTIVITY, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, + EDGE_CONNECTION, + EDGE_COMMUNICATION_FAILURE, NEW_PLATFORM_VERSION(false), ENTITIES_LIMIT(false), API_USAGE_LIMIT(false), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 1a4df035f5..3fe110de0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -81,6 +81,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private String cassandraQueryTenantRateLimitsConfiguration; + private String edgeEventRateLimits; + private String edgeEventRateLimitsPerEdge; + private int defaultStorageTtlDays; private int alarmsTtlDays; private int rpcTtlDays; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java index 3bc2aac1f8..a6b1c11d42 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.Map; import java.util.function.UnaryOperator; +import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.google.common.base.Strings.nullToEmpty; @@ -49,7 +50,7 @@ public class TemplateUtils { value = FUNCTIONS.get(function).apply(value); } } - return value; + return Matcher.quoteReplacement(value); }); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 9dc7110340..2221b236bd 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -22,22 +22,22 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; -import static org.thingsboard.server.common.data.msg.TbMsgType.NA; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE; +import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.MSG_COUNT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.NA; import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_FAILURE; import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_SUCCESS; -import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG; -import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG; -import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG; -import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; class TbMsgTypeTest { - + private static final List typesWithNullRuleNodeConnection = List.of( ALARM, ALARM_DELETE, diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 1e06d75b15..cbf9ae34b3 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -131,6 +131,11 @@ public final class TbMsg implements Serializable { metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, long ts) { + return new TbMsg(null, UUID.randomUUID(), ts, type, originator, null, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + // REALLY NEW MSG /** @@ -335,7 +340,7 @@ public final class TbMsg implements Serializable { this.originator = originator; if (customerId == null || customerId.isNullUid()) { if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) { - this.customerId = (CustomerId) originator; + this.customerId = new CustomerId(originator.getId()); } else { this.customerId = null; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 4da2883291..6e33faaf69 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -551,6 +551,14 @@ message GetOtaPackageResponseMsg { string fileName = 8; } +message DeviceConnectProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; + int64 lastConnectTime = 5; +} + message DeviceActivityProto { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; @@ -559,6 +567,22 @@ message DeviceActivityProto { int64 lastActivityTime = 5; } +message DeviceDisconnectProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; + int64 lastDisconnectTime = 5; +} + +message DeviceInactivityProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 deviceIdMSB = 3; + int64 deviceIdLSB = 4; + int64 lastInactivityTime = 5; +} + //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -1271,6 +1295,9 @@ message ToCoreMsg { LifecycleEventProto lifecycleEventMsg = 8; ErrorEventProto errorEventMsg = 9; ToDeviceActorNotificationMsgProto toDeviceActorNotification = 10; + DeviceConnectProto deviceConnectMsg = 50; + DeviceDisconnectProto deviceDisconnectMsg = 51; + DeviceInactivityProto deviceInactivityMsg = 52; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ @@ -1280,8 +1307,8 @@ message ToCoreNotificationMsg { FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; bytes componentLifecycleMsg = 3 [deprecated = true]; bytes edgeEventUpdateMsg = 4 [deprecated = true]; - QueueUpdateMsg queueUpdateMsg = 5; - QueueDeleteMsg queueDeleteMsg = 6; + repeated QueueUpdateMsg queueUpdateMsgs = 5; + repeated QueueDeleteMsg queueDeleteMsgs = 6; VersionControlResponseMsg vcResponseMsg = 7; bytes toEdgeSyncRequestMsg = 8 [deprecated = true]; bytes fromEdgeSyncResponseMsg = 9 [deprecated = true]; @@ -1307,8 +1334,8 @@ message ToRuleEngineMsg { message ToRuleEngineNotificationMsg { bytes componentLifecycleMsg = 1 [deprecated = true]; FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; - QueueUpdateMsg queueUpdateMsg = 3; - QueueDeleteMsg queueDeleteMsg = 4; + repeated QueueUpdateMsg queueUpdateMsgs = 3; + repeated QueueDeleteMsg queueDeleteMsgs = 4; ComponentLifecycleMsgProto componentLifecycle = 5; } @@ -1328,8 +1355,8 @@ message ToTransportMsg { ResourceUpdateMsg resourceUpdateMsg = 12; ResourceDeleteMsg resourceDeleteMsg = 13; UplinkNotificationMsg uplinkNotificationMsg = 14; - QueueUpdateMsg queueUpdateMsg = 15; - QueueDeleteMsg queueDeleteMsg = 16; + repeated QueueUpdateMsg queueUpdateMsgs = 15; + repeated QueueDeleteMsg queueDeleteMsgs = 16; } message UsageStatsKVProto{ diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java new file mode 100644 index 0000000000..d153998ab4 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/SimpleTbQueueCallback.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common; + +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; + +import java.util.function.Consumer; + +public class SimpleTbQueueCallback implements TbQueueCallback { + + private final Consumer onSuccess; + private final Consumer onFailure; + + public SimpleTbQueueCallback(Consumer onSuccess, Consumer onFailure) { + this.onSuccess = onSuccess; + this.onFailure = onFailure; + } + + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + if (onSuccess != null) { + onSuccess.accept(metadata); + } + } + + @Override + public void onFailure(Throwable t) { + if (onFailure != null) { + onFailure.accept(t); + } + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index c05bc77df3..4630dc4bf8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -171,27 +171,35 @@ public class HashPartitionService implements PartitionService { } @Override - public void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg) { - TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); - partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); - partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); - myPartitions.remove(queueKey); - if (!tenantId.isSysTenantId()) { - tenantRoutingInfoMap.remove(tenantId); + public void updateQueues(List queueUpdateMsgs) { + for (TransportProtos.QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { + TenantId tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); + partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); + partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); + if (!tenantId.isSysTenantId()) { + tenantRoutingInfoMap.remove(tenantId); + } } } @Override - public void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg) { - TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); - myPartitions.remove(queueKey); - partitionTopicsMap.remove(queueKey); - partitionSizesMap.remove(queueKey); - evictTenantInfo(tenantId); + public void removeQueues(List queueDeleteMsgs) { + List queueKeys = queueDeleteMsgs.stream() + .map(queueDeleteMsg -> { + TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); + return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + }) + .collect(Collectors.toList()); + queueKeys.forEach(queueKey -> { + myPartitions.remove(queueKey); + partitionTopicsMap.remove(queueKey); + partitionSizesMap.remove(queueKey); + evictTenantInfo(queueKey.getTenantId()); + }); if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { - publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, Map.of(queueKey, Collections.emptySet())); + publishPartitionChangeEvent(ServiceType.TB_RULE_ENGINE, queueKeys.stream() + .collect(Collectors.toMap(k -> k, k -> Collections.emptySet()))); } } @@ -321,13 +329,11 @@ public class HashPartitionService implements PartitionService { .forEach(removed::add); } removed.forEach(queueKey -> { - log.info("[{}] NO MORE PARTITIONS FOR CURRENT KEY", queueKey); changedPartitionsMap.put(queueKey, Collections.emptySet()); }); myPartitions.forEach((queueKey, partitions) -> { if (!partitions.equals(oldPartitions.get(queueKey))) { - log.info("[{}] NEW PARTITIONS: {}", queueKey, partitions); Set tpiList = partitions.stream() .map(partition -> buildTopicPartitionInfo(queueKey, partition)) .collect(Collectors.toSet()); @@ -373,14 +379,11 @@ public class HashPartitionService implements PartitionService { } private void publishPartitionChangeEvent(ServiceType serviceType, Map> partitionsMap) { - if (log.isDebugEnabled()) { - log.debug("Publishing partition change event for service type " + serviceType + ":" + System.lineSeparator() + - partitionsMap.entrySet().stream() - .map(entry -> entry.getKey() + " - " + entry.getValue().stream() - .map(TopicPartitionInfo::getFullTopicName).sorted() - .collect(Collectors.toList())) - .collect(Collectors.joining(System.lineSeparator()))); - } + log.info("Partitions changed: {}", System.lineSeparator() + partitionsMap.entrySet().stream() + .map(entry -> "[" + entry.getKey() + "] - [" + entry.getValue().stream() + .map(tpi -> tpi.getPartition().orElse(-1).toString()).sorted() + .collect(Collectors.joining(", ")) + "]") + .collect(Collectors.joining(System.lineSeparator()))); PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap); try { applicationEventPublisher.publishEvent(event); @@ -490,7 +493,7 @@ public class HashPartitionService implements PartitionService { } private void logServiceInfo(TransportProtos.ServiceInfo server) { - log.info("[{}] Found common server: [{}]", server.getServiceId(), server.getServiceTypesList()); + log.info("[{}] Found common server: {}", server.getServiceId(), server.getServiceTypesList()); } private void addNode(Map> queueServiceList, ServiceInfo instance) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index 2b2479f59b..e14a6c3947 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -63,9 +63,9 @@ public interface PartitionService { int countTransportsByType(String type); - void updateQueue(TransportProtos.QueueUpdateMsg queueUpdateMsg); + void updateQueues(List queueUpdateMsgs); - void removeQueue(TransportProtos.QueueDeleteMsg queueDeleteMsg); + void removeQueues(List queueDeleteMsgs); void removeTenant(TenantId tenantId); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 735ec1d524..eaf105ee48 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -220,7 +220,7 @@ public class DefaultCoapClientContext implements CoapClientContext { private void onUplink(TbCoapClientState client, boolean notifyOtherServers, long uplinkTs) { PowerMode powerMode = client.getPowerMode(); PowerSavingConfiguration profileSettings = null; - if (powerMode == null) { + if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getProfileId()); if (clientProfile.isPresent()) { profileSettings = clientProfile.get().getClientSettings(); @@ -726,7 +726,7 @@ public class DefaultCoapClientContext implements CoapClientContext { private boolean isDownlinkAllowed(TbCoapClientState client) { PowerMode powerMode = client.getPowerMode(); PowerSavingConfiguration profileSettings = null; - if (powerMode == null) { + if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getProfileId()); if (clientProfile.isPresent()) { profileSettings = clientProfile.get().getClientSettings(); @@ -775,11 +775,12 @@ public class DefaultCoapClientContext implements CoapClientContext { private PowerMode getPowerMode(TbCoapClientState client) { PowerMode powerMode = client.getPowerMode(); if (powerMode == null) { - Optional deviceProfile = getProfile(client.getProfileId()); - if (deviceProfile.isPresent()) { - powerMode = deviceProfile.get().getClientSettings().getPowerMode(); - } else { - powerMode = PowerMode.PSM; + powerMode = PowerMode.PSM; + if (client.getProfileId() != null) { + Optional deviceProfile = getProfile(client.getProfileId()); + if (deviceProfile.isPresent()) { + powerMode = deviceProfile.get().getClientSettings().getPowerMode(); + } } } return powerMode; diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 0f00bbb2c9..530000f95e 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -94,6 +94,11 @@ junit-vintage-engine test + + org.mockito + mockito-inline + test + org.awaitility awaitility diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 4bdcbcf645..a9166b0bf1 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -38,6 +38,7 @@ import javax.annotation.PreDestroy; import java.security.cert.X509Certificate; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; @@ -95,6 +96,7 @@ public class LwM2MTransportBootstrapService { dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); + dtlsConfig.set(DTLS_CONNECTION_ID_LENGTH, serverConfig.getDtlsConnectionIdLength()); dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); setServerWithCredentials(builder, dtlsConfig); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 0ccf6e1969..dc2e9f6d45 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -41,6 +41,10 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.dtls.retransmission_timeout:9000}") private int dtlsRetransmissionTimeout; + @Getter + @Value("${transport.lwm2m.dtls.connection_id_length:6}") + private Integer dtlsConnectionIdLength; + @Getter @Value("${transport.lwm2m.timeout:}") private Long timeout; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 4037251572..7d54e97cde 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -43,6 +43,7 @@ import javax.annotation.PreDestroy; import java.security.cert.X509Certificate; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; @@ -139,6 +140,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups()); dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers()); dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS); + dtlsConfig.set(DTLS_CONNECTION_ID_LENGTH, config.getDtlsConnectionIdLength()); dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); /* Create credentials */ diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 03449aaaef..18e27ba568 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -411,7 +411,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { public boolean isDownlinkAllowed(LwM2mClient client) { PowerMode powerMode = client.getPowerMode(); OtherConfiguration profileSettings = null; - if (powerMode == null) { + if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getProfileId()); profileSettings = clientProfile.getClientLwM2mSettings(); powerMode = profileSettings.getPowerMode(); @@ -419,7 +419,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { powerMode = PowerMode.DRX; } } - if (PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) { + if (powerMode == null || PowerMode.DRX.equals(powerMode) || otaUpdateService.isOtaDownloading(client)) { return true; } client.lock(); @@ -460,7 +460,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { public void onUplink(LwM2mClient client) { PowerMode powerMode = client.getPowerMode(); OtherConfiguration profileSettings = null; - if (powerMode == null) { + if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getProfileId()); profileSettings = clientProfile.getClientLwM2mSettings(); powerMode = profileSettings.getPowerMode(); @@ -468,7 +468,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { powerMode = PowerMode.DRX; } } - if (PowerMode.DRX.equals(powerMode)) { + if (powerMode == null || PowerMode.DRX.equals(powerMode)) { client.updateLastUplinkTime(); return; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index ff86e60490..23c7a57dd6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -29,7 +29,6 @@ import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.redis.RedisRegistrationStore; -import org.eclipse.leshan.server.redis.serialization.IdentitySerDes; import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.registration.Deregistration; @@ -45,6 +44,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.integration.redis.util.RedisLockRegistry; +import org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MIdentitySerDes; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -110,12 +110,18 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit) { + this(connectionFactory, schedExecutor, cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, + new RedisLockRegistry(connectionFactory, "Registration")); + } + + public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, + long lifetimeGracePeriodInSec, int cleanLimit, RedisLockRegistry lockRegistry) { this.connectionFactory = connectionFactory; this.schedExecutor = schedExecutor; this.cleanPeriod = cleanPeriodInSec; this.cleanLimit = cleanLimit; this.gracePeriod = lifetimeGracePeriodInSec; - this.redisLock = new RedisLockRegistry(connectionFactory, "Registration"); + this.redisLock = lockRegistry; } /* *************** Redis Key utility function **************** */ @@ -173,7 +179,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) { removeAddrIndex(connection, oldRegistration); } - if (!oldRegistration.getIdentity().equals(registration.getIdentity())) { + if (registrationsHaveDifferentIdentities(oldRegistration, registration)) { removeIdentityIndex(connection, oldRegistration); } // remove old observation @@ -231,7 +237,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { removeAddrIndex(connection, r); } - if (!r.getIdentity().equals(updatedRegistration.getIdentity())) { + if (registrationsHaveDifferentIdentities(r, updatedRegistration)) { removeIdentityIndex(connection, r); } @@ -402,6 +408,12 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto connection.zRem(EXP_EP, registration.getEndpoint().getBytes(UTF_8)); } + private boolean registrationsHaveDifferentIdentities(Registration first, Registration second){ + var first_identity_string = LwM2MIdentitySerDes.serialize(first.getIdentity()).toString(); + var second_identity_string = LwM2MIdentitySerDes.serialize(second.getIdentity()).toString(); + return !first_identity_string.equals(second_identity_string); + } + private byte[] toRegIdKey(String registrationId) { return toKey(REG_EP_REGID_IDX, registrationId); } @@ -411,7 +423,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } private byte[] toRegIdentityKey(Identity identity) { - return toKey(REG_EP_IDENTITY, IdentitySerDes.serialize(identity).toString()); + return toKey(REG_EP_IDENTITY, LwM2MIdentitySerDes.serialize(identity).toString()); } private byte[] toEndpointKey(String endpoint) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDes.java new file mode 100644 index 0000000000..4ffbfd82bb --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDes.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store.util; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import org.apache.commons.lang3.NotImplementedException; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.util.Hex; + +import java.security.PublicKey; + +public class LwM2MIdentitySerDes { + + private static final String KEY_ADDRESS = "address"; + private static final String KEY_PORT = "port"; + private static final String KEY_ID = "id"; + private static final String KEY_CN = "cn"; + private static final String KEY_RPK = "rpk"; + protected static final String KEY_LWM2MIDENTITY_TYPE = "type"; + protected static final String LWM2MIDENTITY_TYPE_UNSECURE = "unsecure"; + protected static final String LWM2MIDENTITY_TYPE_PSK = "psk"; + protected static final String LWM2MIDENTITY_TYPE_X509 = "x509"; + protected static final String LWM2MIDENTITY_TYPE_RPK = "rpk"; + + public static JsonObject serialize(Identity identity) { + JsonObject o = Json.object(); + + if (identity.isPSK()) { + o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_PSK); + o.set(KEY_ID, identity.getPskIdentity()); + } else if (identity.isRPK()) { + o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_RPK); + PublicKey publicKey = identity.getRawPublicKey(); + o.set(KEY_RPK, Hex.encodeHexString(publicKey.getEncoded())); + } else if (identity.isX509()) { + o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_X509); + o.set(KEY_CN, identity.getX509CommonName()); + } else { + o.set(KEY_LWM2MIDENTITY_TYPE, LWM2MIDENTITY_TYPE_UNSECURE); + o.set(KEY_ADDRESS, identity.getPeerAddress().getHostString()); + o.set(KEY_PORT, identity.getPeerAddress().getPort()); + } + return o; + } + + public static Identity deserialize(JsonObject peer) { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java new file mode 100644 index 0000000000..23293c7b7a --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap; + +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cache.ota.OtaPackageDataCache; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier; +import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore; +import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; +import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; + +@ExtendWith(MockitoExtension.class) +public class LwM2MTransportBootstrapServiceTest { + + @Mock + private LwM2MTransportServerConfig serverConfig; + @Mock + private LwM2MTransportBootstrapConfig bootstrapConfig; + @Mock + private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore; + @Mock + private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; + @Mock + private TransportService transportService; + @Mock + private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; + + + @Test + public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){ + final Integer CONNECTION_ID_LENGTH = 6; + when(serverConfig.getDtlsConnectionIdLength()).thenReturn(CONNECTION_ID_LENGTH); + var lwM2MBootstrapService = createLwM2MBootstrapService(); + + var server = lwM2MBootstrapService.getLhBootstrapServer(); + var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); + assertThat(securedEndpoint).isNotNull(); + + var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); + assertThat(config).isNotNull(); + assertThat(config.getConnectionIdGenerator()).isNotNull(); + assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength")) + .isEqualTo(CONNECTION_ID_LENGTH); + } + + @Test + public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){ + when(serverConfig.getDtlsConnectionIdLength()).thenReturn(null); + var lwM2MBootstrapService = createLwM2MBootstrapService(); + + var server = lwM2MBootstrapService.getLhBootstrapServer(); + var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); + assertThat(securedEndpoint).isNotNull(); + + var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); + assertThat(config).isNotNull(); + assertThat(config.getConnectionIdGenerator()).isNull(); + } + + private LwM2MTransportBootstrapService createLwM2MBootstrapService() { + setDefaultConfigVariables(); + return new LwM2MTransportBootstrapService(serverConfig, bootstrapConfig, lwM2MBootstrapSecurityStore, + lwM2MInMemoryBootstrapConfigStore, transportService, certificateVerifier); + } + + private void setDefaultConfigVariables(){ + when(bootstrapConfig.getPort()).thenReturn(5683); + when(bootstrapConfig.getSecurePort()).thenReturn(5684); + when(serverConfig.isRecommendedCiphers()).thenReturn(false); + when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); + } + + +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigTest.java new file mode 100644 index 0000000000..7e657dd932 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@EnableConfigurationProperties(value = LwM2MTransportServerConfig.class) +@ContextConfiguration(classes = {LwM2MTransportServerConfig.class}, loader = SpringBootContextLoader.class) +@TestPropertySource(properties = { + "transport.sessions.report_timeout=10", + "transport.lwm2m.security.recommended_ciphers=true", + "transport.lwm2m.security.recommended_supported_groups=true", + "transport.lwm2m.downlink_pool_size=10", + "transport.lwm2m.uplink_pool_size=10", + "transport.lwm2m.ota_pool_size=10", + "transport.lwm2m.clean_period_in_sec=2", + "transport.lwm2m.dtls.connection_id_length=" + +}) +class LwM2MTransportServerConfigTest { + + @MockBean(name = "lwm2mServerCredentials") + private SslCredentialsConfig credentialsConfig; + + @MockBean(name = "lwm2mTrustCredentials") + private SslCredentialsConfig trustCredentialsConfig; + + @Autowired + private LwM2MTransportServerConfig serverConfig; + + @Test + void getDtlsConnectionIdLength_return_null_is_property_is_empty() { + // note: transport.lwm2m.dtls.connect_id_length is set in TestPropertySource + assertThat(serverConfig.getDtlsConnectionIdLength()).isNull(); + } +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportServiceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportServiceTest.java new file mode 100644 index 0000000000..92f60ebb8e --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportServiceTest.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cache.ota.OtaPackageDataCache; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; +import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; + +@ExtendWith(MockitoExtension.class) +public class DefaultLwM2mTransportServiceTest { + + @Mock + private LwM2mTransportContext context; + + @Mock + private LwM2MTransportServerConfig config; + @Mock + private OtaPackageDataCache otaPackageDataCache; + @Mock + private LwM2mUplinkMsgHandler handler; + @Mock + private CaliforniumRegistrationStore registrationStore; + @Mock + private TbSecurityStore securityStore; + @Mock + private TbLwM2MDtlsCertificateVerifier certificateVerifier; + @Mock + private TbLwM2MAuthorizer authorizer; + @Mock + private LwM2mVersionedModelProvider modelProvider; + + + @Test + public void getLHServer_creates_ConnectionIdGenerator_when_connection_id_length_not_null(){ + final Integer CONNECTION_ID_LENGTH = 6; + when(config.getDtlsConnectionIdLength()).thenReturn(CONNECTION_ID_LENGTH); + var lwm2mService = createLwM2MService(); + + LeshanServer server = ReflectionTestUtils.invokeMethod(lwm2mService, "getLhServer"); + + assertThat(server).isNotNull(); + var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); + assertThat(securedEndpoint).isNotNull(); + + var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); + assertThat(config).isNotNull(); + assertThat(config.getConnectionIdGenerator()).isNotNull(); + assertThat((Integer) ReflectionTestUtils.getField(config.getConnectionIdGenerator(), "connectionIdLength")) + .isEqualTo(CONNECTION_ID_LENGTH); + } + + @Test + public void getLHServer_creates_no_ConnectionIdGenerator_when_connection_id_length_is_null(){ + when(config.getDtlsConnectionIdLength()).thenReturn(null); + var lwm2mService = createLwM2MService(); + + LeshanServer server = ReflectionTestUtils.invokeMethod(lwm2mService, "getLhServer"); + + assertThat(server).isNotNull(); + var securedEndpoint = (CoapEndpoint) ReflectionTestUtils.getField(server, "securedEndpoint"); + assertThat(securedEndpoint).isNotNull(); + var config = (DtlsConnectorConfig) ReflectionTestUtils.getField(securedEndpoint.getConnector(), "config"); + assertThat(config).isNotNull(); + assertThat(config.getConnectionIdGenerator()).isNull(); + } + + private DefaultLwM2mTransportService createLwM2MService() { + setDefaultConfigVariables(); + return new DefaultLwM2mTransportService(context, config, otaPackageDataCache, handler, registrationStore, + securityStore, certificateVerifier, authorizer, modelProvider); + } + + private void setDefaultConfigVariables(){ + when(config.getPort()).thenReturn(5683); + when(config.getSecurePort()).thenReturn(5684); + when(config.isRecommendedCiphers()).thenReturn(false); + when(config.getDtlsRetransmissionTimeout()).thenReturn(9000); + } + + +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStoreTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStoreTest.java new file mode 100644 index 0000000000..e09f2205b7 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStoreTest.java @@ -0,0 +1,265 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store; + +import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationUpdate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.integration.redis.util.RedisLockRegistry; +import org.springframework.test.util.ReflectionTestUtils; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.locks.Lock; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_CLEAN_LIMIT; +import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_CLEAN_PERIOD; +import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.DEFAULT_GRACE_PERIOD; + + +@ExtendWith(MockitoExtension.class) +class TbLwM2mRedisRegistrationStoreTest { + + RedisConnectionFactory connectionFactory; + RedisConnection connection; + RedisLockRegistry lockRegistry; + + TbLwM2mRedisRegistrationStore registrationStore; + + @BeforeEach + void setUp() { + lockRegistry = mock(RedisLockRegistry.class); + lenient().when(lockRegistry.obtain(any())).thenReturn(mock(Lock.class)); + connection = mock(RedisConnection.class); + //when(connection.set(any(byte[].class), any(byte[].class))). + connectionFactory = mock(RedisConnectionFactory.class); + lenient().when(connectionFactory.getConnection()).thenReturn(connection); + ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, + new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", DEFAULT_CLEAN_PERIOD))); + registrationStore = new TbLwM2mRedisRegistrationStore(connectionFactory, executorService, + DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, lockRegistry); + } + + @Test + void testAddRegistrationWithNoOldRegistration() { + setOldRegistration(null); + Registration registration = buildRegistration(); + + assertThat(registrationStore.addRegistration(registration)).isNull(); + + byte[] endpoint = registration.getEndpoint().getBytes(UTF_8); + verify(connection, times(1)).set(getRegIdKey(registration), endpoint); + verify(connection, times(1)).set(getRegAddrKey(registration), endpoint); + verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint); + verify(connection, times(3)).set(any(byte[].class), any(byte[].class)); + verify(connection, times(0)).del(any(byte[].class)); + } + + @Test + void testAddRegistrationWithOldRegistrationEqualToCurrent(){ + var oldRegistration = buildRegistration(); + setOldRegistration(oldRegistration); + Registration registration = buildRegistration(); + + var deregistration = registrationStore.addRegistration(registration); + + assertThat(deregistration.getRegistration()).isEqualTo(oldRegistration); + + byte[] endpoint = registration.getEndpoint().getBytes(UTF_8); + verify(connection, times(1)).set(getRegIdKey(registration), endpoint); + verify(connection, times(1)).set(getRegAddrKey(registration), endpoint); + verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint); + verify(connection, times(3)).set(any(byte[].class), any(byte[].class)); + verify(connection, times(1)).del(getTknsRegIdKey(oldRegistration)); + verify(connection, times(1)).del(any(byte[].class)); + } + + @Test + void testAddRegistrationRemovesIndexes(){ + var oldRegistration = buildRegistration(Identity.unsecure(getTestAddress(1234))); + setOldRegistration(oldRegistration); + var registration = buildRegistration(Identity.unsecure(getTestAddress(2345))); + + var deregistration = registrationStore.addRegistration(registration); + + assertThat(deregistration.getRegistration()).isEqualTo(oldRegistration); + byte[] endpoint = registration.getEndpoint().getBytes(UTF_8); + verify(connection, times(1)).set(getRegIdKey(registration), endpoint); + verify(connection, times(1)).set(getRegAddrKey(registration), endpoint); + verify(connection, times(1)).set(getRegIdentityKey(registration), endpoint); + verify(connection, times(3)).set(any(byte[].class), any(byte[].class)); + verify(connection, times(1)).del(getRegAddrKey(oldRegistration)); + verify(connection, times(1)).del(getRegIdentityKey(oldRegistration)); + verify(connection, times(1)).del(getTknsRegIdKey(oldRegistration)); + verify(connection, times(3)).del(any(byte[].class)); + } + + @Test + void testUpdateRegistrationWhenNoRegistrationFound() { + setOldRegistration(null); + Registration registration = buildRegistration(); + RegistrationUpdate update = createUpdateFromRegistration(registration); + + assertThat(registrationStore.updateRegistration(update)).isNull(); + + verify(connection, times(1)).get(getRegIdKey(registration)); + verify(connection, times(1)).get(any(byte[].class)); + verify(connection, times(0)).del(any(byte[].class)); + } + + @Test + void testUpdateRegistrationWithSameRegistration() { + Registration registration = buildRegistration(); + setOldRegistration(registration); + RegistrationUpdate update = createUpdateFromRegistration(registration); + + assertThat(registrationStore.updateRegistration(update)).isNotNull(); + + var endpoint = registration.getEndpoint().getBytes(UTF_8); + // check registration and addressIndex here updated + verify(connection, times(1)).set(eq(getEndpointKey(endpoint)), any(byte[].class)); + verify(connection, times(1)).set(getRegAddrKey(registration), endpoint); + verify(connection, times(2)).set(any(byte[].class), any(byte[].class)); + verify(connection, times(0)).del(any(byte[].class)); + } + + @Test + void testUpdateRegistrationWithRegistrationFromSecureIdentitiesWithDifferentAddress() { + Registration oldRegistration = buildRegistration(Identity.psk(getTestAddress(1234), "my:psk")); + setOldRegistration(oldRegistration); + Registration newRegistration = buildRegistration(Identity.psk(getTestAddress(2345), "my:psk")); + RegistrationUpdate update = createUpdateFromRegistration(newRegistration); + assertThat(oldRegistration.getEndpoint()).isEqualTo(newRegistration.getEndpoint()); + + assertThat(registrationStore.updateRegistration(update)).isNotNull(); + + var endpoint = newRegistration.getEndpoint().getBytes(UTF_8); + // check registration and addressIndex here updated + verify(connection, times(1)).set(eq(getEndpointKey(endpoint)), any(byte[].class)); + verify(connection, times(1)).set(getRegAddrKey(newRegistration), endpoint); + // check old AddrIndex has been removed + verify(connection, times(1)).del(getRegAddrKey(oldRegistration)); + // check identityIndex has not been removed + verify(connection, times(0)).del(getRegIdentityKey(oldRegistration)); + // check only one key (AddrIndex) in total was removed + verify(connection, times(1)).del(any(byte[].class)); + } + + @Test + void testGetRegistrationByIdentityReturnsRegistrationForSecureIdentityWithDifferentAddress() { + Registration registration = buildRegistration(Identity.psk(getTestAddress(1234), "my:psk")); + setOldRegistration(registration); + Identity sameIdentityWithDifferentAddress = Identity.psk(getTestAddress(2345), "my:psk"); + + Registration retrievedRegistration = registrationStore.getRegistrationByIdentity(sameIdentityWithDifferentAddress); + + assertThat(retrievedRegistration).isEqualTo(registration); + } + + private void setOldRegistration(Registration oldRegistration){ + byte[] serializedRegistration = null; + if (oldRegistration != null){ + byte[] endpoint = oldRegistration.getEndpoint().getBytes(UTF_8); + // set the AddrIndex + byte[] regAddrKey = getRegAddrKey(oldRegistration); + lenient().when(connection.get(eq(regAddrKey))).thenReturn(endpoint); + // set the IdentityIndex + byte[] regIdentityKey = getRegIdentityKey(oldRegistration); + lenient().when(connection.get(eq(regIdentityKey))).thenReturn(endpoint); + // set the IdIndex + byte[] regIdKey = getRegIdKey(oldRegistration); + lenient().when(connection.get(eq(regIdKey))).thenReturn(endpoint); + // set the registration + serializedRegistration = RegistrationSerDes.bSerialize(oldRegistration); + lenient().when(connection.get(eq(getEndpointKey(endpoint)))).thenReturn(serializedRegistration); + } + lenient().when(connection.getSet(any(byte[].class), any(byte[].class))).thenReturn(serializedRegistration); + } + + private byte[] getRegAddrKey(Registration registration){ + return ReflectionTestUtils.invokeMethod(registrationStore, "toRegAddrKey", registration.getSocketAddress()); + } + + private byte[] getRegIdentityKey(Registration registration){ + return ReflectionTestUtils.invokeMethod(registrationStore, "toRegIdentityKey", registration.getIdentity()); + } + + private byte[] getRegIdKey(Registration registration){ + return ReflectionTestUtils.invokeMethod(registrationStore, "toRegIdKey", registration.getId()); + } + + private byte[] getEndpointKey(byte[] endpoint){ + return ReflectionTestUtils.invokeMethod(registrationStore, "toEndpointKey", endpoint); + } + + private byte[] getTknsRegIdKey(Registration registration){ + return ReflectionTestUtils.invokeMethod(registrationStore, "toKey", "TKNS:REGID:", registration.getId()); + } + + private static Registration buildRegistration() { + return buildRegistration(Identity.psk(getTestAddress(), "my:psk")); + } + + private static Registration buildRegistration(Identity identity){ + return new Registration.Builder("my_reg_id", "abcde", identity) + .objectLinks(new Link[]{}) + .build(); + } + + private static RegistrationUpdate createUpdateFromRegistration(Registration registration){ + return new RegistrationUpdate( + registration.getId(), + registration.getIdentity(), + registration.getLifeTimeInSec(), + registration.getSmsNumber(), + registration.getBindingMode(), + registration.getObjectLinks(), + registration.getAdditionalRegistrationAttributes() + ); + } + + private static InetSocketAddress getTestAddress() { + return getTestAddress(5684); + } + + private static InetSocketAddress getTestAddress(int port) { + try { + return new InetSocketAddress(InetAddress.getByName("1.2.3.4"), port); + } catch (UnknownHostException e) { + throw new AssertionError("Cannot create test address"); + } + } +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDesTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDesTest.java new file mode 100644 index 0000000000..c75ed7a195 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MIdentitySerDesTest.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store.util; + +import com.eclipsesource.json.JsonObject; +import org.apache.commons.lang3.NotImplementedException; +import org.eclipse.leshan.core.request.Identity; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.PublicKey; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LwM2MIdentitySerDesTest { + + @Test + void serializePskIdentity() { + assertThat(LwM2MIdentitySerDes.serialize(Identity.psk(getTestAddress(), "my:psk")).toString()) + .isEqualTo("{\"type\":\"psk\",\"id\":\"my:psk\"}"); + } + + + @Test + void serializeRpkIdentity() { + var public_key = mock(PublicKey.class); + when(public_key.getEncoded()).thenReturn(new byte[]{1,2,3,4,5,6,7,8,9}); + + assertThat(LwM2MIdentitySerDes.serialize(Identity.rpk(getTestAddress(), public_key)).toString()) + .isEqualTo("{\"type\":\"rpk\",\"rpk\":\"010203040506070809\"}"); + } + + @Test + void serializeX509Identity() { + assertThat(LwM2MIdentitySerDes.serialize(Identity.x509(getTestAddress(), "MyCommonName")).toString()) + .isEqualTo("{\"type\":\"x509\",\"cn\":\"MyCommonName\"}"); + } + + @Test + void serializeUnsecureIdentity() { + assertThat(LwM2MIdentitySerDes.serialize(Identity.unsecure(getTestAddress())).toString()) + .isEqualTo("{\"type\":\"unsecure\",\"address\":\"1.2.3.4\",\"port\":5684}"); + } + + + @Test + void deserialize() { + assertThatThrownBy(() -> LwM2MIdentitySerDes.deserialize(mock(JsonObject.class))) + .isInstanceOf(NotImplementedException.class); + } + + private static InetSocketAddress getTestAddress() { + try { + return new InetSocketAddress(InetAddress.getByName("1.2.3.4"), 5684); + } catch (UnknownHostException e) { + throw new AssertionError("Cannot create test address"); + } + } +} \ No newline at end of file diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java index a59013bbfe..6e30b06a2c 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java @@ -152,12 +152,14 @@ public class SnmpTransportContext extends TransportContext { try { if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) { sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration); + sessionContext.setDevice(device); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.createQueryingTasks(sessionContext); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); } else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) { sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration); + sessionContext.setDevice(device); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); } else { diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java index 3a8811df97..3dc293b70b 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java @@ -15,6 +15,11 @@ */ package org.thingsboard.server.transport.snmp.service; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableScheduledFuture; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import lombok.Builder; @@ -32,7 +37,7 @@ import org.snmp4j.mp.MPv3; import org.snmp4j.security.SecurityModels; import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.USM; -import org.snmp4j.smi.Address; +import org.snmp4j.smi.IpAddress; import org.snmp4j.smi.OctetString; import org.snmp4j.smi.TcpAddress; import org.snmp4j.smi.UdpAddress; @@ -44,6 +49,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.common.data.kv.DataType; @@ -53,11 +59,11 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbSnmpTransportComponent; import org.thingsboard.server.transport.snmp.SnmpTransportContext; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; +import org.thingsboard.server.transport.snmp.session.ScheduledTask; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -71,8 +77,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -80,6 +84,7 @@ import java.util.stream.Collectors; @Service @Slf4j @RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") public class SnmpTransportService implements TbTransportService, CommandResponder { private final TransportService transportService; private final PduService pduService; @@ -88,23 +93,27 @@ public class SnmpTransportService implements TbTransportService, CommandResponde @Getter private Snmp snmp; - private ScheduledExecutorService queryingExecutor; - private ExecutorService responseProcessingExecutor; + private ListeningScheduledExecutorService scheduler; + private ExecutorService executor; private final Map responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class); private final Map responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class); @Value("${transport.snmp.bind_port:1620}") private Integer snmpBindPort; - @Value("${transport.snmp.response_processing.parallelism_level}") - private Integer responseProcessingParallelismLevel; + @Value("${transport.snmp.response_processing.parallelism_level:4}") + private int responseProcessingThreadPoolSize; + @Value("${transport.snmp.scheduler_thread_pool_size:4}") + private int schedulerThreadPoolSize; @Value("${transport.snmp.underlying_protocol}") private String snmpUnderlyingProtocol; + @Value("${transport.snmp.request_chunk_delay_ms:100}") + private int requestChunkDelayMs; @PostConstruct private void init() throws IOException { - queryingExecutor = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), ThingsBoardThreadFactory.forName("snmp-querying")); - responseProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(responseProcessingParallelismLevel, "snmp-response-processing"); + scheduler = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(schedulerThreadPoolSize, ThingsBoardThreadFactory.forName("snmp-querying"))); + executor = ThingsBoardExecutors.newWorkStealingPool(responseProcessingThreadPoolSize, "snmp-response-processing"); initializeSnmp(); configureResponseDataMappers(); @@ -115,11 +124,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde @PreDestroy public void stop() { - if (queryingExecutor != null) { - queryingExecutor.shutdownNow(); + if (scheduler != null) { + scheduler.shutdownNow(); } - if (responseProcessingExecutor != null) { - responseProcessingExecutor.shutdownNow(); + if (executor != null) { + executor.shutdownNow(); } } @@ -144,38 +153,39 @@ public class SnmpTransportService implements TbTransportService, CommandResponde } public void createQueryingTasks(DeviceSessionContext sessionContext) { - List> queryingTasks = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() + sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() .filter(communicationConfig -> communicationConfig instanceof RepeatingQueryingSnmpCommunicationConfig) - .map(config -> { + .forEach(config -> { RepeatingQueryingSnmpCommunicationConfig repeatingCommunicationConfig = (RepeatingQueryingSnmpCommunicationConfig) config; Long queryingFrequency = repeatingCommunicationConfig.getQueryingFrequencyMs(); - return queryingExecutor.scheduleWithFixedDelay(() -> { + ScheduledTask scheduledTask = new ScheduledTask(); + scheduledTask.init(() -> { try { if (sessionContext.isActive()) { - sendRequest(sessionContext, repeatingCommunicationConfig); + return sendRequest(sessionContext, repeatingCommunicationConfig); } } catch (Exception e) { log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString()); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e); } - }, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS); - }) - .collect(Collectors.toList()); - sessionContext.getQueryingTasks().addAll(queryingTasks); + return Futures.immediateVoidFuture(); + }, queryingFrequency, scheduler); + sessionContext.getQueryingTasks().add(scheduledTask); + }); } public void cancelQueryingTasks(DeviceSessionContext sessionContext) { - sessionContext.getQueryingTasks().forEach(task -> task.cancel(true)); + sessionContext.getQueryingTasks().forEach(ScheduledTask::cancel); sessionContext.getQueryingTasks().clear(); } - private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig) { - sendRequest(sessionContext, communicationConfig, Collections.emptyMap()); + private ListenableFuture sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig) { + return sendRequest(sessionContext, communicationConfig, Collections.emptyMap()); } - private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map values) { + private ListenableFuture sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map values) { List request = pduService.createPdus(sessionContext, communicationConfig, values); RequestContext requestContext = RequestContext.builder() .communicationSpec(communicationConfig.getSpec()) @@ -183,19 +193,40 @@ public class SnmpTransportService implements TbTransportService, CommandResponde .responseMappings(communicationConfig.getAllMappings()) .requestSize(request.size()) .build(); - sendRequest(sessionContext, request, requestContext); + return sendRequest(sessionContext, request, requestContext); } - private void sendRequest(DeviceSessionContext sessionContext, List request, RequestContext requestContext) { - for (PDU pdu : request) { - log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size()); - try { - snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext); - } catch (IOException e) { - log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString()); - transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e); + private ListenableFuture sendRequest(DeviceSessionContext sessionContext, List request, RequestContext requestContext) { + if (request.size() <= 1 || requestChunkDelayMs == 0) { + for (PDU pdu : request) { + sendPdu(pdu, requestContext, sessionContext); + } + return Futures.immediateVoidFuture(); + } + + List> futures = new ArrayList<>(); + for (int i = 0, delay = 0; i < request.size(); i++, delay += requestChunkDelayMs) { + PDU pdu = request.get(i); + if (delay == 0) { + sendPdu(pdu, requestContext, sessionContext); + } else { + ListenableScheduledFuture future = scheduler.schedule(() -> { + sendPdu(pdu, requestContext, sessionContext); + }, delay, TimeUnit.MILLISECONDS); + futures.add(future); } } + return Futures.whenAllComplete(futures).call(() -> null, MoreExecutors.directExecutor()); + } + + private void sendPdu(PDU pdu, RequestContext requestContext, DeviceSessionContext sessionContext) { + log.debug("[{}] Sending SNMP request with {} variable bindings to {}", sessionContext.getDeviceId(), pdu.size(), sessionContext.getTarget().getAddress()); + try { + snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext); + } catch (Exception e) { + log.error("[{}] Failed to send SNMP request", sessionContext.getDeviceId(), e); + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e); + } } public void onAttributeUpdate(DeviceSessionContext sessionContext, TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotification) { @@ -251,21 +282,19 @@ public class SnmpTransportService implements TbTransportService, CommandResponde ((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext); RequestContext requestContext = (RequestContext) event.getUserObject(); if (event.getError() != null) { - log.warn("SNMP response error: {}", event.getError().toString()); + log.warn("[{}] SNMP response error: {}", sessionContext.getDeviceId(), event.getError().toString()); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError())); return; } PDU responsePdu = event.getResponse(); - if (log.isTraceEnabled()) { - log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu); - } + log.trace("[{}] Received PDU: {}", sessionContext.getDeviceId(), responsePdu); List response; if (requestContext.getRequestSize() == 1) { if (responsePdu == null) { - log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID()); if (requestContext.getMethod() == SnmpMethod.GET) { + log.debug("[{}][{}] Empty response from device", sessionContext.getDeviceId(), event.getRequest().getRequestID()); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device")); } return; @@ -281,14 +310,14 @@ public class SnmpTransportService implements TbTransportService, CommandResponde response.add(responsePart); } } - log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId()); + log.debug("[{}] All {} response parts are collected for request", sessionContext.getDeviceId(), responseParts.size()); } else { - log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId()); + log.trace("[{}] Awaiting other response parts for request", sessionContext.getDeviceId()); return; } } - responseProcessingExecutor.execute(() -> { + executor.execute(() -> { try { processResponse(sessionContext, response, requestContext); } catch (Exception e) { @@ -298,24 +327,31 @@ public class SnmpTransportService implements TbTransportService, CommandResponde } /* - * SNMP notifications handler - * - * TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests) - * - * NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport, - * due to load-balancing of requests from devices: session might not be on this instance - * */ + * SNMP notifications handler + * + * TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests) + * + * NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport, + * due to load-balancing of requests from devices: session might not be on this instance + * */ @Override public void processPdu(CommandResponderEvent event) { - Address sourceAddress = event.getPeerAddress(); - DeviceSessionContext sessionContext = transportContext.getSessions().stream() - .filter(session -> session.getTarget().getAddress().equals(sourceAddress)) - .findFirst().orElse(null); - if (sessionContext == null) { - log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress); + IpAddress sourceAddress = (IpAddress) event.getPeerAddress(); + List sessions = transportContext.getSessions().stream() + .filter(session -> ((IpAddress) session.getTarget().getAddress()).getInetAddress().equals(sourceAddress.getInetAddress())) + .collect(Collectors.toList()); + if (sessions.isEmpty()) { + log.warn("Couldn't find device session for SNMP TRAP for address {}", sourceAddress); + return; + } else if (sessions.size() > 1) { + for (DeviceSessionContext sessionContext : sessions) { + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(), + new IllegalStateException("Found multiple devices for host " + sourceAddress.getInetAddress().getHostAddress())); + } return; } + DeviceSessionContext sessionContext = sessions.get(0); try { processIncomingTrap(sessionContext, event); } catch (Throwable e) { @@ -327,11 +363,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) { PDU pdu = event.getPDU(); if (pdu == null) { - log.warn("Got empty trap from device {}", sessionContext.getDeviceId()); + log.warn("[{}] Received empty SNMP trap", sessionContext.getDeviceId()); throw new IllegalArgumentException("Received TRAP with no data"); } - log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu); + log.debug("[{}] Processing SNMP trap: {}", sessionContext.getDeviceId(), pdu); SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() .filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst() .orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests")); @@ -341,7 +377,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde .method(SnmpMethod.TRAP) .build(); - responseProcessingExecutor.execute(() -> { + executor.execute(() -> { processResponse(sessionContext, List.of(pdu), requestContext); }); } @@ -352,7 +388,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext); if (responseData.size() == 0) { - log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId()); + log.warn("[{}] No values in the response", sessionContext.getDeviceId()); throw new IllegalArgumentException("No values in the response"); } @@ -428,11 +464,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde @PreDestroy public void shutdown() { log.info("Stopping SNMP transport!"); - if (queryingExecutor != null) { - queryingExecutor.shutdownNow(); + if (scheduler != null) { + scheduler.shutdownNow(); } - if (responseProcessingExecutor != null) { - responseProcessingExecutor.shutdownNow(); + if (executor != null) { + executor.shutdownNow(); } if (snmp != null) { try { diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java index 5819c76f74..8ec0c6841d 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java @@ -45,7 +45,6 @@ import org.thingsboard.server.transport.snmp.SnmpTransportContext; import java.util.LinkedList; import java.util.List; import java.util.UUID; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @@ -60,7 +59,8 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S @Setter private SnmpDeviceTransportConfiguration deviceTransportConfiguration; @Getter - private final Device device; + @Setter + private Device device; @Getter private final TenantId tenantId; @@ -73,7 +73,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S private Runnable sessionTimeoutHandler; @Getter - private final List> queryingTasks = new LinkedList<>(); + private final List queryingTasks = new LinkedList<>(); @Builder public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token, diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/ScheduledTask.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/ScheduledTask.java new file mode 100644 index 0000000000..43d83e5e2c --- /dev/null +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/ScheduledTask.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.snmp.session; + +import com.google.common.util.concurrent.AsyncCallable; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Data +@Slf4j +public class ScheduledTask { + private ListenableFuture scheduledFuture; + private boolean stopped = false; + + public void init(AsyncCallable task, long delayMs, ScheduledExecutorService scheduler) { + schedule(task, delayMs, scheduler); + } + + private void schedule(AsyncCallable task, long delayMs, ScheduledExecutorService scheduler) { + scheduledFuture = Futures.scheduleAsync(() -> { + if (stopped) { + return Futures.immediateCancelledFuture(); + } + try { + return task.call(); + } catch (Throwable t) { + log.error("Unhandled error in scheduled task", t); + return Futures.immediateFailedFuture(t); + } + }, delayMs, TimeUnit.MILLISECONDS, scheduler); + if (!stopped) { + scheduledFuture.addListener(() -> schedule(task, delayMs, scheduler), MoreExecutors.directExecutor()); + } + } + + public void cancel() { + stopped = true; + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + } + } + +} diff --git a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java index 261e67bac8..ced8f21288 100644 --- a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java +++ b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java @@ -58,11 +58,10 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { private final Target target; private final Address address; + private final Map mappings; private Snmp snmp; - private final String password; - - public SnmpDeviceSimulatorV2(int port, String password) throws IOException { + public SnmpDeviceSimulatorV2(int port, String password, Map mappings) throws IOException { super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312"))); CommunityTarget target = new CommunityTarget(); target.setCommunity(new OctetString(password)); @@ -72,7 +71,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { target.setTimeout(1500); target.setVersion(SnmpConstants.version2c); this.target = target; - this.password = password; + this.mappings = mappings; } public void start() throws IOException { @@ -85,13 +84,6 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { snmp = new Snmp(transportMappings[0]); } - public void setUpMappings(Map oidToResponseMappings) { - unregisterManagedObject(getSnmpv2MIB()); - oidToResponseMappings.forEach((oid, response) -> { - registerManagedObject(new MOScalar<>(new OID(oid), MOAccessImpl.ACCESS_READ_WRITE, new OctetString(response))); - }); - } - public void sendTrap(String host, int port, Map values) throws IOException { PDU pdu = new PDU(); pdu.addAll(values.entrySet().stream() @@ -107,6 +99,10 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { @Override protected void registerManagedObjects() { + unregisterManagedObject(getSnmpv2MIB()); + mappings.forEach((oid, response) -> { + registerManagedObject(new MOScalar<>(new OID(oid), MOAccessImpl.ACCESS_READ_WRITE, new OctetString(response))); + }); } protected void registerManagedObject(ManagedObject mo) { @@ -152,6 +148,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { } protected void unregisterManagedObjects() { + unregisterManagedObject(getSnmpv2MIB()); } protected void addCommunities(SnmpCommunityMIB communityMIB) { diff --git a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java index cb99c85655..c87507f962 100644 --- a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java +++ b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java @@ -15,28 +15,34 @@ */ package org.thingsboard.server.transport.snmp; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.thingsboard.common.util.JacksonUtil; + +import java.io.File; import java.io.IOException; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Scanner; +import java.util.stream.Collectors; public class SnmpTestV2 { private static final Scanner scanner = new Scanner(System.in); public static void main(String[] args) throws IOException { - SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public"); + Map mappings = new LinkedHashMap<>(); + for (int i = 1; i <= 50; i++) { + String oid = String.format("1.3.6.1.2.1.%s.1.52", i); + mappings.put(oid, "value_" + i); + } - client.start(); - Map mappings = new HashMap<>(); -// for (int i = 1; i <= 500; i++) { -// String oid = String.format(".1.3.6.1.2.1.%s.1.52", i); -// mappings.put(oid, "value_" + i); -// } - mappings.put("1.3.6.1.2.1.266.1.52", "****"); + SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public", mappings); + device.start(); - client.setUpMappings(mappings); - inputTraps(client); + System.out.println("Hosting the following values:\n" + mappings.entrySet().stream() + .map(entry -> entry.getKey() + " - " + entry.getValue()) + .collect(Collectors.joining("\n"))); scanner.nextLine(); } @@ -53,4 +59,18 @@ public class SnmpTestV2 { } } + private static void updateDeviceProfile(String file) throws Exception { + File profileFile = new File(file); + JsonNode deviceProfile = JacksonUtil.OBJECT_MAPPER.readTree(profileFile); + ArrayNode mappingsJson = (ArrayNode) deviceProfile.at("/profileData/transportConfiguration/communicationConfigs/0/mappings"); + for (int i = 1; i <= 50; i++) { + String oid = String.format(".1.3.6.1.2.1.%s.1.52", i); + mappingsJson.add(JacksonUtil.newObjectNode() + .put("oid", oid) + .put("key", "key_" + i) + .put("dataType", "STRING")); + } + JacksonUtil.OBJECT_MAPPER.writeValue(profileFile, deviceProfile); + } + } diff --git a/common/transport/snmp/src/test/resources/snmp-device-profile-transport-config.json b/common/transport/snmp/src/test/resources/snmp-device-profile-transport-config.json deleted file mode 100644 index f74ebca0bf..0000000000 --- a/common/transport/snmp/src/test/resources/snmp-device-profile-transport-config.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "timeoutMs": 500, - "retries": 0, - "communicationConfigs": [ - { - "spec": "TELEMETRY_QUERYING", - "queryingFrequencyMs": 3000, - "mappings": [ - { - "oid": ".1.3.6.1.2.1.1.1.50", - "key": "temperature", - "dataType": "LONG" - }, - { - "oid": ".1.3.6.1.2.1.2.1.52", - "key": "humidity", - "dataType": "DOUBLE" - } - ] - }, - { - "spec": "CLIENT_ATTRIBUTES_QUERYING", - "queryingFrequencyMs": 5000, - "mappings": [ - { - "oid": ".1.3.6.1.2.1.3.1.54", - "key": "isCool", - "dataType": "STRING" - } - ] - }, - { - "spec": "SHARED_ATTRIBUTES_SETTING", - "mappings": [ - { - "oid": ".1.3.6.1.2.1.7.1.58", - "key": "shared", - "dataType": "STRING" - } - ] - } - ] -} diff --git a/common/transport/snmp/src/test/resources/snmp-device-transport-config-v3.json b/common/transport/snmp/src/test/resources/snmp-device-transport-config-v3.json deleted file mode 100644 index 039e03fa53..0000000000 --- a/common/transport/snmp/src/test/resources/snmp-device-transport-config-v3.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "address": "192.168.3.23", - "port": 1610, - "protocolVersion": "V3", - - "username": "tb-user", - "engineId": "qwertyuioa", - "securityName": "tb-user", - "authenticationProtocol": "SHA_512", - "authenticationPassphrase": "sdfghjkloifgh", - "privacyProtocol": "DES", - "privacyPassphrase": "rtytguijokod" -} \ No newline at end of file diff --git a/common/transport/snmp/src/test/resources/snmp-device-transport-config.json b/common/transport/snmp/src/test/resources/snmp-device-transport-config.json deleted file mode 100644 index c73d817bfb..0000000000 --- a/common/transport/snmp/src/test/resources/snmp-device-transport-config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "address": "127.0.0.1", - "port": 1610, - "community": "public", - "protocolVersion": "V2C" -} \ No newline at end of file diff --git a/common/transport/snmp/src/test/resources/snmp_device_profile.json b/common/transport/snmp/src/test/resources/snmp_device_profile.json new file mode 100644 index 0000000000..0e72c46ce8 --- /dev/null +++ b/common/transport/snmp/src/test/resources/snmp_device_profile.json @@ -0,0 +1,289 @@ +{ + "name": "SNMP Device Profile", + "description": "", + "image": null, + "type": "DEFAULT", + "transportType": "SNMP", + "provisionType": "DISABLED", + "defaultRuleChainId": null, + "defaultDashboardId": null, + "defaultQueueName": null, + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "type": "SNMP", + "timeoutMs": 500, + "retries": 0, + "communicationConfigs": [ + { + "spec": "TELEMETRY_QUERYING", + "mappings": [ + { + "oid": ".1.3.6.1.2.1.1.1.52", + "key": "key_1", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.2.1.52", + "key": "key_2", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.3.1.52", + "key": "key_3", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.4.1.52", + "key": "key_4", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.5.1.52", + "key": "key_5", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.6.1.52", + "key": "key_6", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.7.1.52", + "key": "key_7", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.8.1.52", + "key": "key_8", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.9.1.52", + "key": "key_9", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.10.1.52", + "key": "key_10", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.11.1.52", + "key": "key_11", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.12.1.52", + "key": "key_12", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.13.1.52", + "key": "key_13", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.14.1.52", + "key": "key_14", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.15.1.52", + "key": "key_15", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.16.1.52", + "key": "key_16", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.17.1.52", + "key": "key_17", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.18.1.52", + "key": "key_18", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.19.1.52", + "key": "key_19", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.20.1.52", + "key": "key_20", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.21.1.52", + "key": "key_21", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.22.1.52", + "key": "key_22", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.23.1.52", + "key": "key_23", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.24.1.52", + "key": "key_24", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.25.1.52", + "key": "key_25", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.26.1.52", + "key": "key_26", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.27.1.52", + "key": "key_27", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.28.1.52", + "key": "key_28", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.29.1.52", + "key": "key_29", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.30.1.52", + "key": "key_30", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.31.1.52", + "key": "key_31", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.32.1.52", + "key": "key_32", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.33.1.52", + "key": "key_33", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.34.1.52", + "key": "key_34", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.35.1.52", + "key": "key_35", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.36.1.52", + "key": "key_36", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.37.1.52", + "key": "key_37", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.38.1.52", + "key": "key_38", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.39.1.52", + "key": "key_39", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.40.1.52", + "key": "key_40", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.41.1.52", + "key": "key_41", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.42.1.52", + "key": "key_42", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.43.1.52", + "key": "key_43", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.44.1.52", + "key": "key_44", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.45.1.52", + "key": "key_45", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.46.1.52", + "key": "key_46", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.47.1.52", + "key": "key_47", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.48.1.52", + "key": "key_48", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.49.1.52", + "key": "key_49", + "dataType": "STRING" + }, + { + "oid": ".1.3.6.1.2.1.50.1.52", + "key": "key_50", + "dataType": "STRING" + } + ], + "queryingFrequencyMs": 5000 + } + ] + }, + "provisionConfiguration": { + "type": "DISABLED", + "provisionDeviceSecret": null + }, + "alarms": null + }, + "provisionDeviceKey": null, + "firmwareId": null, + "softwareId": null, + "defaultEdgeRuleChainId": null, + "default": false +} \ No newline at end of file diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 0d7fbfc332..4aa989ed25 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -788,7 +788,11 @@ public class DefaultTransportService extends TransportActivityManager implements .setSuccess(success) .setError(error != null ? ExceptionUtils.getStackTrace(error) : "")) .build(); - sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + try { + sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + } catch (Exception e) { + log.error("[{}][{}] Failed to send lifecycle event to core", tenantId, deviceId, e); + } } @Override @@ -801,9 +805,13 @@ public class DefaultTransportService extends TransportActivityManager implements .setEntityIdLSB(deviceId.getId().getLeastSignificantBits()) .setServiceId(serviceInfoProvider.getServiceId()) .setMethod(method) - .setError(ExceptionUtils.getStackTrace(error))) + .setError(ExceptionUtils.getRootCauseMessage(error))) .build(); - sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + try { + sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + } catch (Exception e) { + log.error("[{}][{}] Failed to send error event to core", tenantId, deviceId, e); + } } @Override @@ -985,10 +993,10 @@ public class DefaultTransportService extends TransportActivityManager implements log.warn("ResourceDelete - [{}] [{}]", id, mdRez); transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg)); }); - } else if (toSessionMsg.hasQueueUpdateMsg()) { - partitionService.updateQueue(toSessionMsg.getQueueUpdateMsg()); - } else if (toSessionMsg.hasQueueDeleteMsg()) { - partitionService.removeQueue(toSessionMsg.getQueueDeleteMsg()); + } else if (toSessionMsg.getQueueUpdateMsgsCount() > 0) { + partitionService.updateQueues(toSessionMsg.getQueueUpdateMsgsList()); + } else if (toSessionMsg.getQueueDeleteMsgsCount() > 0) { + partitionService.removeQueues(toSessionMsg.getQueueDeleteMsgsList()); } else { //TODO: should we notify the device actor about missed session? log.debug("[{}] Missing session.", sessionId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index f190923167..2f7fe0680c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -157,16 +157,19 @@ public class DeviceServiceImpl extends AbstractCachedEntityService deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true); } + @Transactional @Override public Device saveDeviceWithAccessToken(Device device, String accessToken) { return doSaveDevice(device, accessToken, true); } + @Transactional @Override public Device saveDevice(Device device, boolean doValidate) { return doSaveDevice(device, null, doValidate); } + @Transactional @Override public Device saveDevice(Device device) { return doSaveDevice(device, null, true); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java index 9ea70f6c5a..4d1e126c49 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java @@ -19,11 +19,15 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.limits.RateLimitService; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.service.DataValidator; @Service @@ -32,11 +36,17 @@ import org.thingsboard.server.dao.service.DataValidator; public class BaseEdgeEventService implements EdgeEventService { private final EdgeEventDao edgeEventDao; - + private final RateLimitService rateLimitService; private final DataValidator edgeEventValidator; @Override public ListenableFuture saveAsync(EdgeEvent edgeEvent) { + if (!rateLimitService.checkRateLimit(LimitedApi.EDGE_EVENTS, edgeEvent.getTenantId())) { + throw new TbRateLimitsException(EntityType.TENANT); + } + if (!rateLimitService.checkRateLimit(LimitedApi.EDGE_EVENTS_PER_EDGE, edgeEvent.getTenantId(), edgeEvent.getEdgeId())) { + throw new TbRateLimitsException(EntityType.EDGE); + } edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId); return edgeEventDao.saveAsync(edgeEvent); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 80eea0a478..136fd0485d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -30,10 +30,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -48,14 +48,15 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; @@ -64,7 +65,7 @@ import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; -import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import javax.annotation.Nullable; @@ -105,7 +106,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService edgeValidator; @@ -113,6 +117,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService isEdgeActiveAsync(TenantId tenantId, EdgeId edgeId, String key) { + ListenableFuture> futureKvEntry; + if (persistToTelemetry) { + futureKvEntry = timeseriesService.findLatest(tenantId, edgeId, key); + } else { + futureKvEntry = attributesService.find(tenantId, edgeId, DataConstants.SERVER_SCOPE, key); + } + return Futures.transformAsync(futureKvEntry, kvEntryOpt -> + Futures.immediateFuture(kvEntryOpt.flatMap(KvEntry::getBooleanValue).orElse(false)), MoreExecutors.directExecutor()); + } + private List findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { List result = new ArrayList<>(); PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java index 23d4245575..e569745504 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import javax.annotation.PreDestroy; diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java index 822768c572..59fdcac935 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import javax.annotation.PreDestroy; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 59787a01a7..c858eecf62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; @@ -53,6 +54,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -187,6 +189,8 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId()); defaultNotifications.create(tenantId, DefaultNotifications.ruleEngineComponentLifecycleFailure, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.edgeConnection, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.edgeCommunicationFailures, tenantAdmins.getId()); } @Override @@ -198,17 +202,43 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS } NotificationTarget sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.SYSTEM_ADMINISTRATORS).stream() - .findFirst().orElseGet(() -> { - return createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators"); - }); + .findFirst().orElseGet(() -> createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators")); NotificationTarget affectedTenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.AFFECTED_TENANT_ADMINISTRATORS).stream() - .findFirst().orElseGet(() -> { - return createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), ""); - }); + .findFirst().orElseGet(() -> createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), "")); defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId()); + } else { + var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE); + var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes( + tenantId, requiredNotificationTypes, new PageLink(1)) + .getData() + .stream() + .map(NotificationTemplate::getNotificationType) + .collect(Collectors.toSet()); + + if (existingNotificationTypes.containsAll(requiredNotificationTypes)) { + return; + } + + NotificationTarget tenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.TENANT_ADMINISTRATORS) + .stream() + .findFirst() + .orElseGet(() -> createTarget(tenantId, "Tenant administrators", new TenantAdministratorsFilter(), "Tenant administrators")); + + for (NotificationType type : requiredNotificationTypes) { + if (!existingNotificationTypes.contains(type)) { + switch (type) { + case EDGE_CONNECTION: + defaultNotifications.create(tenantId, DefaultNotifications.edgeConnection, tenantAdmins.getId()); + break; + case EDGE_COMMUNICATION_FAILURE: + defaultNotifications.create(tenantId, DefaultNotifications.edgeCommunicationFailures, tenantAdmins.getId()); + break; + } + } + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 5a78fa9864..fd17618bd1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -40,6 +40,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig; @@ -325,6 +328,35 @@ public class DefaultNotifications { .description("Send notification to tenant admins when any Rule chain or Rule node failed to start, update or stop") .build()) .build(); + public static final DefaultNotification edgeConnection = DefaultNotification.builder() + .name("Edge connection notification") + .type(NotificationType.EDGE_CONNECTION) + .subject("Edge connection status change") + .text("Edge '${edgeName}' is now ${eventType}") + .icon("info").color(null) + .button("Go to Edge").link("/edgeManagement/instances/${edgeId}") + .rule(DefaultRule.builder() + .name("Edge connection status change") + .triggerConfig(EdgeConnectionNotificationRuleTriggerConfig.builder() + .edges(null) + .notifyOn(Set.of(EdgeConnectivityEvent.CONNECTED, EdgeConnectivityEvent.DISCONNECTED)) + .build()) + .description("Send notification to tenant admins when the connection status between TB and Edge changes") + .build()) + .build(); + public static final DefaultNotification edgeCommunicationFailures = DefaultNotification.builder() + .name("Edge communication failure notification") + .type(NotificationType.EDGE_COMMUNICATION_FAILURE) + .subject("Edge '${edgeName}' communication failure occurred") + .text("Failure message: '${failureMsg}'") + .icon("error").color(RED_COLOR) + .button("Go to Edge").link("/edgeManagement/instances/${edgeId}") + .rule(DefaultRule.builder() + .name("Edge communication failure") + .triggerConfig(EdgeCommunicationFailureNotificationRuleTriggerConfig.builder().edges(null).build()) + .description("Send notification to tenant admins when communication failures occur") + .build()) + .build(); public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() .name("JWT Signing Key issue notification") @@ -346,7 +378,7 @@ public class DefaultNotifications { if (defaultNotification.getRule() != null && targets.length > 0) { NotificationRule rule = defaultNotification.toRule(template.getId(), targets); rule.setTenantId(tenantId); - rule = ruleService.saveNotificationRule(tenantId, rule); + ruleService.saveNotificationRule(tenantId, rule); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index 57c45b1d0b..0755fb1526 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -57,9 +57,6 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Autowired private DataValidator queueValidator; -// @Autowired -// private QueueStatsService queueStatsService; - @Override public Queue saveQueue(Queue queue) { log.trace("Executing createOrUpdateQueue [{}]", queue); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 1d1296c42e..962e0429df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -149,6 +149,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override + @Transactional public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java index 4610973370..a9d118befb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/DefaultTbTenantProfileCache.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.tenant; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.limits.TenantProfileProvider; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; @@ -31,7 +32,7 @@ import java.util.function.Consumer; @Service @Slf4j -public class DefaultTbTenantProfileCache implements TbTenantProfileCache { +public class DefaultTbTenantProfileCache implements TbTenantProfileCache, TenantProfileProvider { private final Lock tenantProfileFetchLock = new ReentrantLock(); private final TenantProfileService tenantProfileService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 31e29434e8..6b2ab7e07b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -65,6 +65,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -85,6 +86,18 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD public static final String ASC_ORDER = "ASC"; public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1); protected static final List FIXED_PARTITION = List.of(0L); + protected static final String INSERT_WITH_NULL = INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + ModelConstants.BOOLEAN_VALUE_COLUMN + + "," + ModelConstants.STRING_VALUE_COLUMN + + "," + ModelConstants.LONG_VALUE_COLUMN + + "," + ModelConstants.DOUBLE_VALUE_COLUMN + + "," + ModelConstants.JSON_VALUE_COLUMN + ")" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; private CassandraTsPartitionsCache cassandraTsPartitionsCache; @@ -117,6 +130,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement[] fetchStmtsAsc; private PreparedStatement[] fetchStmtsDesc; private PreparedStatement deleteStmt; + private PreparedStatement saveWithNullStmt; + private PreparedStatement saveWithNullWithTtlStmt; private final Lock stmtCreationLock = new ReentrantLock(); private boolean isInstall() { @@ -159,19 +174,36 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD ttl = computeTtl(ttl); int dataPointDays = tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY)); long partition = toPartitionTs(tsKvEntry.getTs()); + String entityType = entityId.getEntityType().name(); + UUID entityIdId = entityId.getId(); + String entryKey = tsKvEntry.getKey(); + long ts = tsKvEntry.getTs(); DataType type = tsKvEntry.getDataType(); + BoundStatementBuilder stmtBuilder; if (setNullValuesEnabled) { - processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type); - } - BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); - stmtBuilder.setString(0, entityId.getEntityType().name()) - .setUuid(1, entityId.getId()) - .setString(2, tsKvEntry.getKey()) - .setLong(3, partition) - .setLong(4, tsKvEntry.getTs()); - addValue(tsKvEntry, stmtBuilder, 5); - if (ttl > 0) { - stmtBuilder.setInt(6, (int) ttl); + Boolean booleanValue = tsKvEntry.getBooleanValue().orElse(null); + String strValue = tsKvEntry.getStrValue().orElse(null); + Long longValue = tsKvEntry.getLongValue().orElse(null); + Double doubleValue = tsKvEntry.getDoubleValue().orElse(null); + String jsonValue = tsKvEntry.getJsonValue().orElse(null); + if (ttl == 0) { + stmtBuilder = new BoundStatementBuilder(getSaveWithNullStmt() + .bind(entityType, entityIdId, entryKey, partition, ts, booleanValue, strValue, longValue, doubleValue, jsonValue)); + } else { + stmtBuilder = new BoundStatementBuilder(getSaveWithNullWithTtlStmt() + .bind(entityType, entityIdId, entryKey, partition, ts, booleanValue, strValue, longValue, doubleValue, jsonValue, (int) ttl)); + } + } else { + stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); + stmtBuilder.setString(0, entityType) + .setUuid(1, entityIdId) + .setString(2, entryKey) + .setLong(3, partition) + .setLong(4, ts); + addValue(tsKvEntry, stmtBuilder, 5); + if (ttl > 0) { + stmtBuilder.setInt(6, (int) ttl); + } } BoundStatement stmt = stmtBuilder.build(); futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null)); @@ -449,56 +481,6 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD return tsFormat.getTruncateUnit().equals(ChronoUnit.FOREVER); } - private void processSetNullValues(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, List> futures, long partition, DataType type) { - switch (type) { - case LONG: - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON)); - break; - case BOOLEAN: - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON)); - break; - case DOUBLE: - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON)); - break; - case STRING: - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON)); - break; - case JSON: - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG)); - futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING)); - break; - } - } - - private ListenableFuture saveNull(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, long partition, DataType type) { - BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); - stmtBuilder.setString(0, entityId.getEntityType().name()) - .setUuid(1, entityId.getId()) - .setString(2, tsKvEntry.getKey()) - .setLong(3, partition) - .setLong(4, tsKvEntry.getTs()); - stmtBuilder.setToNull(getColumnName(type)); - if (ttl > 0) { - stmtBuilder.setInt(6, (int) ttl); - } - BoundStatement stmt = stmtBuilder.build(); - return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); - } - private ListenableFuture doSavePartition(TenantId tenantId, EntityId entityId, String key, long ttl, long partition) { log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); PreparedStatement preparedStatement = ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt(); @@ -591,6 +573,34 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD return deleteStmt; } + private PreparedStatement getSaveWithNullStmt() { + if (saveWithNullStmt == null) { + stmtCreationLock.lock(); + try { + if (saveWithNullStmt == null) { + saveWithNullStmt = prepare(INSERT_WITH_NULL); + } + } finally { + stmtCreationLock.unlock(); + } + } + return saveWithNullStmt; + } + + private PreparedStatement getSaveWithNullWithTtlStmt() { + if (saveWithNullWithTtlStmt == null) { + stmtCreationLock.lock(); + try { + if (saveWithNullWithTtlStmt == null) { + saveWithNullWithTtlStmt = prepare(INSERT_WITH_NULL + " USING TTL ?"); + } + } finally { + stmtCreationLock.unlock(); + } + } + return saveWithNullWithTtlStmt; + } + private PreparedStatement getSaveStmt(DataType dataType) { if (saveStmts == null) { stmtCreationLock.lock(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index ebe2d59dcf..8474af9584 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -38,7 +38,7 @@ import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.dao.util.limits.RateLimitService; +import org.thingsboard.server.cache.limits.RateLimitService; import javax.annotation.Nullable; import java.util.HashMap; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java index b0d42cdb07..f4a77dc725 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java @@ -112,12 +112,10 @@ public class DeviceConnectivityUtil { dockerComposeBuilder.append("# - \"5026:5026\" # Modbus TCP connector (Modbus Slave)\n"); dockerComposeBuilder.append("# - \"50000:50000/tcp\" # Socket connector with type TCP\n"); dockerComposeBuilder.append("# - \"50000:50000/udp\" # Socket connector with type UDP\n"); - if (isLocalhost(host)) { - dockerComposeBuilder.append("\n"); - dockerComposeBuilder.append(" # Necessary mapping for Linux\n"); - dockerComposeBuilder.append(" extra_hosts:\n"); - dockerComposeBuilder.append(" - \"host.docker.internal:host-gateway\"\n"); - } + dockerComposeBuilder.append("\n"); + dockerComposeBuilder.append(" # Necessary mapping for Linux\n"); + dockerComposeBuilder.append(" extra_hosts:\n"); + dockerComposeBuilder.append(" - \"host.docker.internal:host-gateway\"\n"); dockerComposeBuilder.append("\n"); dockerComposeBuilder.append(" # Environment variables\n"); dockerComposeBuilder.append(" environment:\n"); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 61d1c845fc..12bcac72f1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -16,12 +16,15 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.hibernate.exception.ConstraintViolationException; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @@ -35,8 +38,6 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; @@ -50,15 +51,19 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; import org.thingsboard.server.dao.tenant.TenantProfileService; import java.nio.ByteBuffer; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -79,6 +84,8 @@ public class DeviceServiceTest extends AbstractServiceTest { TenantProfileService tenantProfileService; @Autowired private PlatformTransactionManager platformTransactionManager; + @SpyBean + private DeviceCredentialsDataValidator validator; private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; @@ -129,6 +136,67 @@ public class DeviceServiceTest extends AbstractServiceTest { }); } + @Test + public void testSaveDevicesWithTheSameAccessToken() { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + String accessToken = StringUtils.generateSafeToken(10); + Device savedDevice = deviceService.saveDeviceWithAccessToken(device, accessToken); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); + Assert.assertEquals(accessToken, deviceCredentials.getCredentialsId()); + + Device duplicatedDevice = new Device(); + duplicatedDevice.setTenantId(tenantId); + duplicatedDevice.setName(StringUtils.randomAlphabetic(10)); + duplicatedDevice.setType("default"); + assertThatThrownBy(() -> deviceService.saveDeviceWithAccessToken(duplicatedDevice, accessToken)) + .isInstanceOf(DeviceCredentialsValidationException.class) + .hasMessageContaining("Device credentials are already assigned to another device!"); + + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, duplicatedDevice.getName()); + Assertions.assertNull(deviceByName); + } + + @Test + public void testShouldRollbackNotValidatedDeviceIfDeviceCredentialsValidationFailed() { + Mockito.reset(validator); + Mockito.doThrow(new DataValidationException("mock message")) + .when(validator).validate(any(), any()); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + assertThatThrownBy(() -> deviceService.saveDevice(device, false)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("mock message"); + + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, device.getName()); + Assertions.assertNull(deviceByName); + } + + @Test + public void testShouldRollbackValidatedDeviceIfDeviceCredentialsValidationFailed() { + Mockito.reset(validator); + Mockito.doThrow(new DataValidationException("mock message")) + .when(validator).validate(any(), any()); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + + assertThatThrownBy(() -> deviceService.saveDevice(device)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("mock message"); + + Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, device.getName()); + Assertions.assertNull(deviceByName); + } + @Test public void testCountByTenantId() { Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java index 2a03a1277c..2cc179fae2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java @@ -40,7 +40,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.List; -import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; +import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT; @DaoSqlTest public class EdgeEventServiceTest extends AbstractServiceTest { @@ -56,19 +56,18 @@ public class EdgeEventServiceTest extends AbstractServiceTest { @Before public void before() throws ParseException { - timeBeforeStartTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T11:30:00Z").getTime(); - startTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:00:00Z").getTime(); - eventTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:30:00Z").getTime(); - endTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:00:00Z").getTime(); - timeAfterEndTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:30:30Z").getTime(); + timeBeforeStartTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T11:30:00").getTime(); + startTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T12:00:00").getTime(); + eventTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T12:30:00").getTime(); + endTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T13:00:00").getTime(); + timeAfterEndTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T13:30:30").getTime(); } @Test public void saveEdgeEvent() throws Exception { EdgeId edgeId = new EdgeId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased()); - TenantId tenantId = new TenantId(Uuids.timeBased()); - EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED); + EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId); edgeEventService.saveAsync(edgeEvent).get(); PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1)); @@ -81,9 +80,11 @@ public class EdgeEventServiceTest extends AbstractServiceTest { Assert.assertEquals(saved.getType(), edgeEvent.getType()); Assert.assertEquals(saved.getAction(), edgeEvent.getAction()); Assert.assertEquals(saved.getBody(), edgeEvent.getBody()); + + edgeEventService.cleanupEvents(1); } - protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType edgeEventAction) throws IOException { + protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId) throws IOException { if (tenantId == null) { tenantId = TenantId.fromUUID(Uuids.timeBased()); } @@ -92,7 +93,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest { edgeEvent.setEdgeId(edgeId); edgeEvent.setEntityId(entityId.getId()); edgeEvent.setType(EdgeEventType.DEVICE); - edgeEvent.setAction(edgeEventAction); + edgeEvent.setAction(EdgeEventActionType.ADDED); edgeEvent.setBody(readFromResource("TestJsonData.json")); return edgeEvent; } @@ -101,7 +102,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest { public void findEdgeEventsByTimeDescOrder() throws Exception { EdgeId edgeId = new EdgeId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased()); - TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); List> futures = new ArrayList<>(); futures.add(saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId)); @@ -133,7 +133,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest { } private ListenableFuture saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { - EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); + EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId); edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); return edgeEventService.saveAsync(edgeEvent); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index ad351bb9f2..a624a2b6e5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -54,7 +55,9 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * @author Andrew Shvayka @@ -64,12 +67,12 @@ import static org.junit.Assert.assertNotNull; public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Autowired - TimeseriesService tsService; + protected TimeseriesService tsService; @Autowired EntityViewService entityViewService; - static final int MAX_TIMEOUT = 30; + protected static final int MAX_TIMEOUT = 30; private static final String STRING_KEY = "stringKey"; private static final String LONG_KEY = "longKey"; @@ -84,7 +87,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE); KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); - private TenantId tenantId; + protected TenantId tenantId; @Before public void before() { @@ -673,6 +676,32 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { assertEquals(3, list.size()); } + @Test + public void shouldSaveEntryOfEachType() throws Exception { + BasicTsKvEntry booleanEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(1), new BooleanDataEntry("test", true)); + BasicTsKvEntry stringEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(2), new StringDataEntry("test", "text")); + BasicTsKvEntry longEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(3), new LongDataEntry("test", 15L)); + BasicTsKvEntry doubleEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(4), new DoubleDataEntry("test", 10.5)); + BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")); + List timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry); + + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + for (TsKvEntry tsKvEntry : timeseries) { + save(tenantId, deviceId, tsKvEntry); + } + + List listUntil3Minutes = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L, + TimeUnit.MINUTES.toMillis(3), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(2, listUntil3Minutes.size()); + assertThat(listUntil3Minutes).containsOnlyOnceElementsOf(List.of( + booleanEntry, stringEntry)); + + List fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L, + TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(5, fullList.size()); + assertThat(fullList).containsOnlyOnceElementsOf(timeseries); + } + private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception { TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value)); tsService.save(tenantId, deviceId, entry).get(MAX_TIMEOUT, TimeUnit.SECONDS); @@ -691,6 +720,9 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { return entry; } + private void save(TenantId tenantId, DeviceId deviceId, TsKvEntry tsKvEntry) throws Exception { + tsService.save(tenantId, deviceId, tsKvEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + } private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException, TimeoutException { tsService.save(tenantId, deviceId, toTsEntry(ts, stringKvEntry)).get(MAX_TIMEOUT, TimeUnit.SECONDS); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlSetNullEnabledTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlSetNullEnabledTest.java new file mode 100644 index 0000000000..1bbb37659c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlSetNullEnabledTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.timeseries.nosql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.Test; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.service.DaoNoSqlTest; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@DaoNoSqlTest +@TestPropertySource(properties = { + "cassandra.query.set_null_values_enabled=true", +}) +public class TimeseriesServiceNoSqlSetNullEnabledTest extends TimeseriesServiceNoSqlTest { + + @Override + @Test + public void testNullValuesOfNoneTargetColumn() throws ExecutionException, InterruptedException, TimeoutException { + long ts = TimeUnit.MINUTES.toMillis(1); + TsKvEntry longEntry = new BasicTsKvEntry(ts, new LongDataEntry("temp", 0L)); + double doubleValue = 20.6; + TsKvEntry doubleEntry = new BasicTsKvEntry(ts, new DoubleDataEntry("temp", doubleValue)); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + tsService.save(tenantId, deviceId, longEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + tsService.save(tenantId, deviceId, doubleEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + List listWithoutAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L, + ts + 1 , 1000, 3, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(1, listWithoutAgg.size()); + assertFalse(listWithoutAgg.get(0).getLongValue().isPresent()); + assertTrue(listWithoutAgg.get(0).getDoubleValue().isPresent()); + assertThat(listWithoutAgg.get(0).getDoubleValue().get()).isEqualTo(doubleValue); + + // long value should be set to null after second insert, so avg = doubleValue + List listWithAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L, + ts + 1 , 1000, 3, Aggregation.AVG))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(1, listWithAgg.size()); + assertTrue(listWithAgg.get(0).getDoubleValue().isPresent()); + assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(doubleValue); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java index 229d5f5842..b8970feca3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java @@ -15,9 +15,83 @@ */ package org.thingsboard.server.dao.service.timeseries.nosql; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.Test; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.service.DaoNoSqlTest; import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + @DaoNoSqlTest public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest { + + @Test + public void shouldSaveEntryOfEachTypeWithTtl() throws ExecutionException, InterruptedException, TimeoutException { + long ttlInSec = TimeUnit.SECONDS.toSeconds(3); + List timeseries = List.of( + new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(1), new BooleanDataEntry("test", true)), + new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(2), new StringDataEntry("test", "text")), + new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(3), new LongDataEntry("test", 15L)), + new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(4), new DoubleDataEntry("test", 10.5)), + new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}"))); + + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + tsService.save(tenantId, deviceId, timeseries, ttlInSec); + + List fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L, + TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(5, fullList.size()); + + // check entries after ttl + Thread.sleep(TimeUnit.SECONDS.toMillis(ttlInSec + 1)); + List listAfterTtl = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L, + TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(0, listAfterTtl.size()); + } + + @Test + public void testNullValuesOfNoneTargetColumn() throws ExecutionException, InterruptedException, TimeoutException { + long ts = TimeUnit.MINUTES.toMillis(1); + long longValue = 10L; + TsKvEntry longEntry = new BasicTsKvEntry(ts, new LongDataEntry("temp", longValue)); + double doubleValue = 20.6; + TsKvEntry doubleEntry = new BasicTsKvEntry(ts, new DoubleDataEntry("temp", doubleValue)); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + tsService.save(tenantId, deviceId, longEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + tsService.save(tenantId, deviceId, doubleEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + List listWithoutAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L, + ts + 1 , 1000, 3, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(1, listWithoutAgg.size()); + assertTrue(listWithoutAgg.get(0).getLongValue().isPresent()); + assertFalse(listWithoutAgg.get(0).getDoubleValue().isPresent()); + assertThat(listWithoutAgg.get(0).getLongValue().get()).isEqualTo(longValue); + + // long value should not be reset to null, so avg = (doubleValue + longValue)/ 2 + List listWithAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L, + ts + 1, 200000, 3, Aggregation.AVG))).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertEquals(1, listWithAgg.size()); + assertTrue(listWithAgg.get(0).getDoubleValue().isPresent()); + double expectedValue = (doubleValue + longValue)/ 2; + assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(expectedValue); + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java index 8ee73110a0..eef0126f7f 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java @@ -21,6 +21,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; import org.thingsboard.common.util.ListeningExecutor; public interface MqttClient { @@ -32,7 +33,7 @@ public interface MqttClient { * @param host The ip address or host to connect to * @return A future which will be completed when the connection is opened and we received an CONNACK */ - Future connect(String host); + Promise connect(String host); /** * Connect to the specified hostname/ip using the specified port @@ -41,7 +42,7 @@ public interface MqttClient { * @param port The tcp port to connect to * @return A future which will be completed when the connection is opened and we received an CONNACK */ - Future connect(String host, int port); + Promise connect(String host, int port); /** * @@ -55,7 +56,7 @@ public interface MqttClient { * @return A future which will be completed when the connection is opened and we received an CONNACK * @throws IllegalStateException if no previous {@link #connect(String, int)} calls were attempted */ - Future reconnect(); + Promise reconnect(); /** * Retrieve the netty {@link EventLoopGroup} we are using diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index dedc670490..6781bb171a 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -118,7 +118,7 @@ final class MqttClientImpl implements MqttClient { * @return A future which will be completed when the connection is opened and we received an CONNACK */ @Override - public Future connect(String host) { + public Promise connect(String host) { return connect(host, 1883); } @@ -130,11 +130,11 @@ final class MqttClientImpl implements MqttClient { * @return A future which will be completed when the connection is opened and we received an CONNACK */ @Override - public Future connect(String host, int port) { + public Promise connect(String host, int port) { return connect(host, port, false); } - private Future connect(String host, int port, boolean reconnect) { + private Promise connect(String host, int port, boolean reconnect) { log.trace("[{}] Connecting to server, isReconnect - {}", channel != null ? channel.id() : "UNKNOWN", reconnect); if (this.eventLoop == null) { this.eventLoop = new NioEventLoopGroup(); @@ -199,7 +199,7 @@ final class MqttClientImpl implements MqttClient { } @Override - public Future reconnect() { + public Promise reconnect() { log.trace("[{}] Reconnecting to server, isReconnect - {}", channel != null ? channel.id() : "UNKNOWN", reconnect); if (host == null) { throw new IllegalStateException("Cannot reconnect. Call connect() first"); diff --git a/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java b/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java index 8a398eeec5..04103bf67b 100644 --- a/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java +++ b/netty-mqtt/src/test/java/org/thingsboard/mqtt/integration/MqttIntegrationTest.java @@ -21,6 +21,7 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; @@ -127,7 +128,7 @@ public class MqttIntegrationTest { config.setReconnectDelay(RECONNECT_DELAY_SECONDS); MqttClient client = MqttClient.create(config, null, handlerExecutor); client.setEventLoop(this.eventLoopGroup); - Future connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort()); + Promise connectFuture = client.connect(MQTT_HOST, this.mqttServer.getMqttPort()); String hostPort = MQTT_HOST + ":" + this.mqttServer.getMqttPort(); MqttConnectResult result; diff --git a/pom.xml b/pom.xml index 6efb293c41..3a5e64db9e 100755 --- a/pom.xml +++ b/pom.xml @@ -131,6 +131,7 @@ 2.7.2 1.5.2 5.8.2 + 4.5.1 2.6.0 5.13.1 1.3.0 @@ -1649,6 +1650,12 @@ + + org.mockito + mockito-inline + ${mockito.version} + test + org.testng testng diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index f3a7ee77c7..8783770be3 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -525,12 +525,11 @@ public class RestClient implements Closeable { } public PageData getAlarmComments(AlarmId alarmId, PageLink pageLink) { - String urlSecondPart = "/api/alarm/{alarmId}/comment"; Map params = new HashMap<>(); params.put("alarmId", alarmId.getId().toString()); - + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + urlSecondPart + "&" + getUrlParams(pageLink), + baseURL + "/api/alarm/{alarmId}/comment?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java new file mode 100644 index 0000000000..82c2916408 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceStateManager.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api; + +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; + +public interface RuleEngineDeviceStateManager { + + void onDeviceConnect(TenantId tenantId, DeviceId deviceId, long connectTime, TbCallback callback); + + void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long activityTime, TbCallback callback); + + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId, long disconnectTime, TbCallback callback); + + void onDeviceInactivity(TenantId tenantId, DeviceId deviceId, long inactivityTime, TbCallback callback); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 59f5b128a7..fc2980650b 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -280,6 +280,10 @@ public interface TbContext { DeviceCredentialsService getDeviceCredentialsService(); + RuleEngineDeviceStateManager getDeviceStateManager(); + + String getDeviceStateNodeRateLimitConfig(); + TbClusterService getClusterService(); DashboardService getDashboardService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java new file mode 100644 index 0000000000..7925178b33 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -0,0 +1,173 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.action; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.tools.TbRateLimits; + +import java.util.EnumSet; +import java.util.Set; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "device state", + nodeDescription = "Triggers device connectivity events", + nodeDetails = "If incoming message originator is a device, registers configured event for that device in the Device State Service, which sends appropriate message to the Rule Engine." + + " If metadata ts property is present, it will be used as event timestamp. Otherwise, the message timestamp will be used." + + " If originator entity type is not DEVICE or unexpected error happened during processing, then incoming message is forwarded using Failure chain." + + " If rate of connectivity events for a given originator is too high, then incoming message is forwarded using Rate limited chain. " + + "
" + + "Supported device connectivity events are:" + + "
    " + + "
  • Connect event
  • " + + "
  • Disconnect event
  • " + + "
  • Activity event
  • " + + "
  • Inactivity event
  • " + + "
" + + "This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.", + configClazz = TbDeviceStateNodeConfiguration.class, + relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, "Rate limited"}, + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeDeviceStateConfig" +) +public class TbDeviceStateNode implements TbNode { + + private static final Set SUPPORTED_EVENTS = EnumSet.of( + TbMsgType.CONNECT_EVENT, TbMsgType.ACTIVITY_EVENT, TbMsgType.DISCONNECT_EVENT, TbMsgType.INACTIVITY_EVENT + ); + private static final String DEFAULT_RATE_LIMIT_CONFIG = "1:1,30:60,60:3600"; + private ConcurrentReferenceHashMap rateLimits; + private String rateLimitConfig; + private TbMsgType event; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + TbMsgType event = TbNodeUtils.convert(configuration, TbDeviceStateNodeConfiguration.class).getEvent(); + if (event == null) { + throw new TbNodeException("Event cannot be null!", true); + } + if (!SUPPORTED_EVENTS.contains(event)) { + throw new TbNodeException("Unsupported event: " + event, true); + } + this.event = event; + rateLimits = new ConcurrentReferenceHashMap<>(); + String deviceStateNodeRateLimitConfig = ctx.getDeviceStateNodeRateLimitConfig(); + try { + rateLimitConfig = new TbRateLimits(deviceStateNodeRateLimitConfig).getConfiguration(); + } catch (Exception e) { + log.error("[{}][{}] Invalid rate limit configuration provided: [{}]. Will use default value [{}].", + ctx.getTenantId().getId(), ctx.getSelfId().getId(), deviceStateNodeRateLimitConfig, DEFAULT_RATE_LIMIT_CONFIG, e); + rateLimitConfig = DEFAULT_RATE_LIMIT_CONFIG; + } + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + EntityType originatorEntityType = msg.getOriginator().getEntityType(); + if (!EntityType.DEVICE.equals(originatorEntityType)) { + ctx.tellFailure(msg, new IllegalArgumentException( + "Unsupported originator entity type: [" + originatorEntityType + "]. Only DEVICE entity type is supported." + )); + return; + } + DeviceId originator = new DeviceId(msg.getOriginator().getId()); + rateLimits.compute(originator, (__, rateLimit) -> { + if (rateLimit == null) { + rateLimit = new TbRateLimits(rateLimitConfig); + } + boolean isNotRateLimited = rateLimit.tryConsume(); + if (isNotRateLimited) { + sendEventAndTell(ctx, originator, msg); + } else { + ctx.tellNext(msg, "Rate limited"); + } + return rateLimit; + }); + } + + private void sendEventAndTell(TbContext ctx, DeviceId originator, TbMsg msg) { + TenantId tenantId = ctx.getTenantId(); + long eventTs = msg.getMetaDataTs(); + + RuleEngineDeviceStateManager deviceStateManager = ctx.getDeviceStateManager(); + TbCallback callback = getMsgEnqueuedCallback(ctx, msg); + + switch (event) { + case CONNECT_EVENT: + deviceStateManager.onDeviceConnect(tenantId, originator, eventTs, callback); + break; + case ACTIVITY_EVENT: + deviceStateManager.onDeviceActivity(tenantId, originator, eventTs, callback); + break; + case DISCONNECT_EVENT: + deviceStateManager.onDeviceDisconnect(tenantId, originator, eventTs, callback); + break; + case INACTIVITY_EVENT: + deviceStateManager.onDeviceInactivity(tenantId, originator, eventTs, callback); + break; + default: + ctx.tellFailure(msg, new IllegalStateException("Configured event [" + event + "] is not supported!")); + } + } + + private TbCallback getMsgEnqueuedCallback(TbContext ctx, TbMsg msg) { + return new TbCallback() { + @Override + public void onSuccess() { + ctx.tellSuccess(msg); + } + + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + }; + } + + @Override + public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { + rateLimits.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); + } + + @Override + public void destroy() { + if (rateLimits != null) { + rateLimits.clear(); + rateLimits = null; + } + rateLimitConfig = null; + event = null; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java new file mode 100644 index 0000000000..c23338fcd1 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.action; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.msg.TbMsgType; + +@Data +public class TbDeviceStateNodeConfiguration implements NodeConfiguration { + + private TbMsgType event; + + @Override + public TbDeviceStateNodeConfiguration defaultConfiguration() { + var config = new TbDeviceStateNodeConfiguration(); + config.setEvent(TbMsgType.ACTIVITY_EVENT); + return config; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 31fda250d0..4176ead31c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -47,6 +47,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.TO_SERVER_RPC_REQUEST; @Slf4j public abstract class AbstractTbMsgPushNode implements TbNode { @@ -176,6 +177,6 @@ public abstract class AbstractTbMsgPushNode
" + + "If an object with coordinates extracted from incoming message enters the geofence, sends a message with the type Entered. " + + "If an object leaves the geofence, sends a message with the type Left. " + + "If the presence monitoring strategy \"On first message\" is selected, sends messages via rule node connection type Inside or Outside only the first time the geofencing and duration conditions are satisfied; otherwise sends messages via rule node connection type Success. " + + "If the presence monitoring strategy \"On each message\" is selected, sends messages via rule node connection type Inside or Outside every time the geofencing condition is satisfied. " + + "

" + + "Output connections: Entered, Left, Inside, Outside, Success", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeGpsGeofencingConfig" ) public class TbGpsGeofencingActionNode extends AbstractGeofencingNode { + private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage"; private final Map entityStates = new HashMap<>(); private final Gson gson = new Gson(); private final JsonParser parser = new JsonParser(); @@ -80,25 +97,32 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode (entityState.isInside() ? - TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) { - setStaid(ctx, msg.getOriginator(), entityState); - ctx.tellNext(msg, entityState.isInside() ? "Inside" : "Outside"); - told = true; - } - } + ctx.tellNext(msg, matches ? ENTERED : LEFT); + return; + } + + if (config.isReportPresenceStatusOnEachMessage()) { + ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE); + return; } - if (!told) { + + if (entityState.isStayed()) { ctx.tellSuccess(msg); + return; + } + + long stayTime = ts - entityState.getStateSwitchTime(); + if (stayTime > (entityState.isInside() ? + TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : + TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) { + setStaid(ctx, msg.getOriginator(), entityState); + ctx.tellNext(msg, entityState.isInside() ? INSIDE : OUTSIDE); + return; } + + ctx.tellSuccess(msg); } private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) { @@ -127,4 +151,17 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode getConfigClazz() { return TbGpsGeofencingActionNodeConfiguration.class; } + + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + if (fromVersion == 0) { + if (!oldConfiguration.has(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE)) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put(REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE, false); + } + } + return new TbPair<>(hasChanges, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java index d0adad8996..04bbab01b7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java @@ -31,6 +31,8 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte private String minInsideDurationTimeUnit; private String minOutsideDurationTimeUnit; + private boolean reportPresenceStatusOnEachMessage; + @Override public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() { TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration(); @@ -43,6 +45,7 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name()); configuration.setMinInsideDuration(1); configuration.setMinOutsideDuration(1); + configuration.setReportPresenceStatusOnEachMessage(true); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 7632402e5e..6e80a1577b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -18,7 +18,7 @@ package org.thingsboard.rule.engine.mqtt; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; -import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; import lombok.extern.slf4j.Slf4j; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -121,7 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode { prepareMqttClientConfig(config); MqttClient client = MqttClient.create(config, null, ctx.getExternalCallExecutor()); client.setEventLoop(ctx.getSharedEventLoop()); - Future connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); + Promise connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); MqttConnectResult result; try { result = connectFuture.get(this.mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java index 80d73b9478..4d651f48fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -63,6 +64,9 @@ public class EntitiesFieldsAsyncLoader { case ENTITY_VIEW: return toEntityFieldsDataAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) originatorId), EntityFieldsData::new, ctx); + case EDGE: + return toEntityFieldsDataAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) originatorId), + EntityFieldsData::new, ctx); default: return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originatorId.getEntityType())); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java new file mode 100644 index 0000000000..db5ddaf698 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/GpsGeofencingEvents.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.util; + +public class GpsGeofencingEvents { + public static final String ENTERED = "Entered"; + public static final String INSIDE = "Inside"; + public static final String LEFT = "Left"; + public static final String OUTSIDE = "Outside"; +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java new file mode 100644 index 0000000000..9352d64809 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeviceStateNodeTest.java @@ -0,0 +1,296 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.action; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineDeviceStateManager; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.tools.TbRateLimits; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class TbDeviceStateNodeTest { + + @Mock + private TbContext ctxMock; + @Mock + private static RuleEngineDeviceStateManager deviceStateManagerMock; + @Captor + private static ArgumentCaptor callbackCaptor; + private TbDeviceStateNode node; + private TbDeviceStateNodeConfiguration config; + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final long METADATA_TS = 123L; + private TbMsg msg; + + @BeforeEach + public void setup() { + var metaData = new TbMsgMetaData(); + metaData.putValue("deviceName", "My humidity sensor"); + metaData.putValue("deviceType", "Humidity sensor"); + metaData.putValue("ts", String.valueOf(METADATA_TS)); + var data = JacksonUtil.newObjectNode(); + data.put("humidity", 58.3); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, JacksonUtil.toString(data)); + } + + @BeforeEach + public void setUp() { + node = new TbDeviceStateNode(); + config = new TbDeviceStateNodeConfiguration().defaultConfiguration(); + } + + @Test + public void givenDefaultConfiguration_whenInvoked_thenCorrectValuesAreSet() { + assertThat(config.getEvent()).isEqualTo(TbMsgType.ACTIVITY_EVENT); + } + + @Test + public void givenNullEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN-WHEN-THEN + assertThatThrownBy(() -> initNode(null)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Event cannot be null!") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @Test + public void givenInvalidRateLimitConfig_whenInit_thenUsesDefaultConfig() { + // GIVEN + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("invalid rate limit config"); + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getSelfId()).willReturn(new RuleNodeId(UUID.randomUUID())); + + // WHEN + try { + initNode(TbMsgType.ACTIVITY_EVENT); + } catch (Exception e) { + fail("Node failed to initialize!", e); + } + + // THEN + String actualRateLimitConfig = (String) ReflectionTestUtils.getField(node, "rateLimitConfig"); + assertThat(actualRateLimitConfig).isEqualTo("1:1,30:60,60:3600"); + } + + @Test + public void givenMsgArrivedTooFast_whenOnMsg_thenRateLimitsThisMsg() { + // GIVEN + ConcurrentReferenceHashMap rateLimits = new ConcurrentReferenceHashMap<>(); + ReflectionTestUtils.setField(node, "rateLimits", rateLimits); + + var rateLimitMock = mock(TbRateLimits.class); + rateLimits.put(DEVICE_ID, rateLimitMock); + + given(rateLimitMock.tryConsume()).willReturn(false); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().tellNext(msg, "Rate limited"); + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).tellFailure(any(), any()); + then(ctxMock).shouldHaveNoMoreInteractions(); + then(deviceStateManagerMock).shouldHaveNoInteractions(); + } + + @Test + public void givenHasNonLocalDevices_whenOnPartitionChange_thenRemovesEntriesForNonLocalDevices() { + // GIVEN + ConcurrentReferenceHashMap rateLimits = new ConcurrentReferenceHashMap<>(); + ReflectionTestUtils.setField(node, "rateLimits", rateLimits); + + rateLimits.put(DEVICE_ID, new TbRateLimits("1:1")); + given(ctxMock.isLocalEntity(eq(DEVICE_ID))).willReturn(true); + + DeviceId nonLocalDeviceId1 = new DeviceId(UUID.randomUUID()); + rateLimits.put(nonLocalDeviceId1, new TbRateLimits("2:2")); + given(ctxMock.isLocalEntity(eq(nonLocalDeviceId1))).willReturn(false); + + DeviceId nonLocalDeviceId2 = new DeviceId(UUID.randomUUID()); + rateLimits.put(nonLocalDeviceId2, new TbRateLimits("3:3")); + given(ctxMock.isLocalEntity(eq(nonLocalDeviceId2))).willReturn(false); + + // WHEN + node.onPartitionChangeMsg(ctxMock, new PartitionChangeMsg(ServiceType.TB_RULE_ENGINE)); + + // THEN + assertThat(rateLimits) + .containsKey(DEVICE_ID) + .doesNotContainKey(nonLocalDeviceId1) + .doesNotContainKey(nonLocalDeviceId2) + .size().isOne(); + } + + @ParameterizedTest + @EnumSource( + value = TbMsgType.class, + names = {"CONNECT_EVENT", "ACTIVITY_EVENT", "DISCONNECT_EVENT", "INACTIVITY_EVENT"}, + mode = EnumSource.Mode.EXCLUDE + ) + public void givenUnsupportedEventInConfig_whenInit_thenThrowsUnrecoverableTbNodeException(TbMsgType unsupportedEvent) { + // GIVEN-WHEN-THEN + assertThatThrownBy(() -> initNode(unsupportedEvent)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Unsupported event: " + unsupportedEvent) + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @ParameterizedTest + @EnumSource(value = EntityType.class, names = "DEVICE", mode = EnumSource.Mode.EXCLUDE) + public void givenNonDeviceOriginator_whenOnMsg_thenTellsSuccessAndNoActivityActionsTriggered(EntityType unsupportedType) { + // GIVEN + var nonDeviceOriginator = new EntityId() { + + @Override + public UUID getId() { + return UUID.randomUUID(); + } + + @Override + public EntityType getEntityType() { + return unsupportedType; + } + }; + var msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, nonDeviceOriginator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + then(ctxMock).should().tellFailure(eq(msg), exceptionCaptor.capture()); + assertThat(exceptionCaptor.getValue()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported originator entity type: [" + unsupportedType + "]. Only DEVICE entity type is supported."); + + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenMetadataDoesNotContainTs_whenOnMsg_thenMsgTsIsUsedAsEventTs() { + // GIVEN + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("1:1"); + try { + initNode(TbMsgType.ACTIVITY_EVENT); + } catch (TbNodeException e) { + fail("Node failed to initialize!", e); + } + + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); + + long msgTs = METADATA_TS + 1; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, msgTs); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(msgTs), any()); + } + + @ParameterizedTest + @MethodSource + public void givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback(TbMsgType supportedEventType, Runnable actionVerification) { + // GIVEN + given(ctxMock.getTenantId()).willReturn(TENANT_ID); + given(ctxMock.getDeviceStateNodeRateLimitConfig()).willReturn("1:1"); + given(ctxMock.getDeviceStateManager()).willReturn(deviceStateManagerMock); + + try { + initNode(supportedEventType); + } catch (TbNodeException e) { + fail("Node failed to initialize!", e); + } + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + actionVerification.run(); + + TbCallback actualCallback = callbackCaptor.getValue(); + + actualCallback.onSuccess(); + then(ctxMock).should().tellSuccess(msg); + + var throwable = new Throwable(); + actualCallback.onFailure(throwable); + then(ctxMock).should().tellFailure(msg, throwable); + + + then(deviceStateManagerMock).shouldHaveNoMoreInteractions(); + then(ctxMock).shouldHaveNoMoreInteractions(); + } + + private static Stream givenSupportedEventAndDeviceOriginator_whenOnMsg_thenCorrectEventIsSentWithCorrectCallback() { + return Stream.of( + Arguments.of(TbMsgType.CONNECT_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceConnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.ACTIVITY_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceActivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.DISCONNECT_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceDisconnect(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())), + Arguments.of(TbMsgType.INACTIVITY_EVENT, (Runnable) () -> then(deviceStateManagerMock).should().onDeviceInactivity(eq(TENANT_ID), eq(DEVICE_ID), eq(METADATA_TS), callbackCaptor.capture())) + ); + } + + private void initNode(TbMsgType event) throws TbNodeException { + config.setEvent(event); + var nodeConfig = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfig); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java new file mode 100644 index 0000000000..804d30d28f --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.geo; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class GpsGeofencingActionTestCase { + + private EntityId entityId; + private Map entityStates; + private boolean msgInside; + private boolean reportPresenceStatusOnEachMessage; + + public GpsGeofencingActionTestCase(EntityId entityId, boolean msgInside, boolean reportPresenceStatusOnEachMessage, EntityGeofencingState entityGeofencingState) { + this.entityId = entityId; + this.msgInside = msgInside; + this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage; + this.entityStates = new HashMap<>(); + this.entityStates.put(entityId, entityGeofencingState); + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java new file mode 100644 index 0000000000..5cd7e02c59 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeTest.java @@ -0,0 +1,259 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.geo; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.attributes.AttributesService; + +import java.time.Duration; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.ENTERED; +import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE; +import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT; +import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS; + +@ExtendWith(MockitoExtension.class) +class TbGpsGeofencingActionNodeTest extends AbstractRuleNodeUpgradeTest { + + @Mock + private TbContext ctx; + @Mock + private AttributesService attributesService; + private TbGpsGeofencingActionNode node; + + @BeforeEach + void setUp() { + node = spy(new TbGpsGeofencingActionNode()); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + private static Stream givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + long tsNow = System.currentTimeMillis(); + long tsNowMinusMinuteAndMillis = tsNow - Duration.ofMinutes(1).plusMillis(1).toMillis(); + return Stream.of( + // default config with presenceMonitoringStrategyOnEachMessage false and msgInside true + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, + new EntityGeofencingState(false, 0, false)), ENTERED), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, + new EntityGeofencingState(true, tsNow, false)), SUCCESS), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, + new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, false, + new EntityGeofencingState(true, tsNow, true)), SUCCESS), + // default config with presenceMonitoringStrategyOnEachMessage false and msgInside false + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, + new EntityGeofencingState(false, 0, false)), LEFT), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, + new EntityGeofencingState(false, tsNow, false)), SUCCESS), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, + new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, false, + new EntityGeofencingState(false, tsNow, true)), SUCCESS), + // default config with presenceMonitoringStrategyOnEachMessage true and msgInside true + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, + new EntityGeofencingState(false, 0, false)), ENTERED), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, + new EntityGeofencingState(true, tsNow, false)), INSIDE), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, true, true, + new EntityGeofencingState(true, tsNowMinusMinuteAndMillis, false)), INSIDE), + // default config with presenceMonitoringStrategyOnEachMessage true and msgInside false + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, + new EntityGeofencingState(false, 0, false)), LEFT), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, + new EntityGeofencingState(false, tsNow, false)), OUTSIDE), + Arguments.of(new GpsGeofencingActionTestCase(deviceId, false, true, + new EntityGeofencingState(false, tsNowMinusMinuteAndMillis, false)), OUTSIDE) + ); + } + + @ParameterizedTest + @MethodSource + void givenReportPresenceStatusOnEachMessage_whenOnMsg_thenVerifyOutputMsgType( + GpsGeofencingActionTestCase gpsGeofencingActionTestCase, + String expectedOutput + ) throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingActionNodeConfiguration().defaultConfiguration(); + config.setReportPresenceStatusOnEachMessage(gpsGeofencingActionTestCase.isReportPresenceStatusOnEachMessage()); + + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + TbMsg msg = gpsGeofencingActionTestCase.isMsgInside() ? + getInsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId()) : + getOutsideRectangleTbMsg(gpsGeofencingActionTestCase.getEntityId()); + + when(ctx.getAttributesService()).thenReturn(attributesService); + + ReflectionTestUtils.setField(node, "entityStates", gpsGeofencingActionTestCase.getEntityStates()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + verify(ctx.getAttributesService(), never()).find(any(), any(), any(), anyString()); + verify(ctx, never()).tellFailure(any(), any(Throwable.class)); + verify(ctx, never()).enqueueForTellNext(any(), eq(expectedOutput), any(), any()); + verify(ctx, never()).ack(any()); + + if (SUCCESS.equals(expectedOutput)) { + verify(ctx).tellSuccess(eq(msg)); + } else { + verify(ctx).tellNext(eq(msg), eq(expectedOutput)); + } + } + + private TbMsg getOutsideRectangleTbMsg(EntityId entityId) { + return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(), + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + } + + private TbMsg getInsideRectangleTbMsg(EntityId entityId) { + return getTbMsg(entityId, getMetadataForNewVersionPolygonPerimeter(), + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + } + + private TbMsg getTbMsg(EntityId entityId, TbMsgMetaData metadata, double latitude, double longitude) { + String data = "{\"latitude\": " + latitude + ", \"longitude\": " + longitude + "}"; + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metadata, data); + } + + private TbMsgMetaData getMetadataForNewVersionPolygonPerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("ss_perimeter", GeoUtilTest.SIMPLE_RECT); + return metadata; + } + + // Rule nodes upgrade + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + // default config for version 0 + Arguments.of(0, + "{\n" + + " \"minInsideDuration\": 1,\n" + + " \"minOutsideDuration\": 1,\n" + + " \"minInsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"latitudeKeyName\": \"latitude\",\n" + + " \"longitudeKeyName\": \"longitude\",\n" + + " \"perimeterType\": \"POLYGON\",\n" + + " \"fetchPerimeterInfoFromMessageMetadata\": true,\n" + + " \"perimeterKeyName\": \"ss_perimeter\",\n" + + " \"polygonsDefinition\": null,\n" + + " \"centerLatitude\": null,\n" + + " \"centerLongitude\": null,\n" + + " \"range\": null,\n" + + " \"rangeUnit\": null\n" + + "}\n", + true, + "{\n" + + " \"minInsideDuration\": 1,\n" + + " \"minOutsideDuration\": 1,\n" + + " \"minInsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"reportPresenceStatusOnEachMessage\": false,\n" + + " \"latitudeKeyName\": \"latitude\",\n" + + " \"longitudeKeyName\": \"longitude\",\n" + + " \"perimeterType\": \"POLYGON\",\n" + + " \"fetchPerimeterInfoFromMessageMetadata\": true,\n" + + " \"perimeterKeyName\": \"ss_perimeter\",\n" + + " \"polygonsDefinition\": null,\n" + + " \"centerLatitude\": null,\n" + + " \"centerLongitude\": null,\n" + + " \"range\": null,\n" + + " \"rangeUnit\": null\n" + + "}\n"), + // default config for version 1 with upgrade from version 0 + Arguments.of(0, + "{\n" + + " \"minInsideDuration\": 1,\n" + + " \"minOutsideDuration\": 1,\n" + + " \"minInsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"reportPresenceStatusOnEachMessage\": false,\n" + + " \"latitudeKeyName\": \"latitude\",\n" + + " \"longitudeKeyName\": \"longitude\",\n" + + " \"perimeterType\": \"POLYGON\",\n" + + " \"fetchPerimeterInfoFromMessageMetadata\": true,\n" + + " \"perimeterKeyName\": \"ss_perimeter\",\n" + + " \"polygonsDefinition\": null,\n" + + " \"centerLatitude\": null,\n" + + " \"centerLongitude\": null,\n" + + " \"range\": null,\n" + + " \"rangeUnit\": null\n" + + "}\n", + false, + "{\n" + + " \"minInsideDuration\": 1,\n" + + " \"minOutsideDuration\": 1,\n" + + " \"minInsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"minOutsideDurationTimeUnit\": \"MINUTES\",\n" + + " \"reportPresenceStatusOnEachMessage\": false,\n" + + " \"latitudeKeyName\": \"latitude\",\n" + + " \"longitudeKeyName\": \"longitude\",\n" + + " \"perimeterType\": \"POLYGON\",\n" + + " \"fetchPerimeterInfoFromMessageMetadata\": true,\n" + + " \"perimeterKeyName\": \"ss_perimeter\",\n" + + " \"polygonsDefinition\": null,\n" + + " \"centerLatitude\": null,\n" + + " \"centerLongitude\": null,\n" + + " \"range\": null,\n" + + " \"rangeUnit\": null\n" + + "}\n") + ); + } + + @Override + protected TbNode getTestNode() { + return node; + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java index 5967023354..f3e61dcffc 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java @@ -37,10 +37,12 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -52,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -95,6 +98,8 @@ public class EntitiesFieldsAsyncLoaderTest { private RuleChainService ruleChainServiceMock; @Mock private EntityViewService entityViewServiceMock; + @Mock + private EdgeService edgeServiceMock; @BeforeAll public static void setup() { @@ -108,7 +113,8 @@ public class EntitiesFieldsAsyncLoaderTest { EntityType.DEVICE, EntityType.ALARM, EntityType.RULE_CHAIN, - EntityType.ENTITY_VIEW + EntityType.ENTITY_VIEW, + EntityType.EDGE ); } @@ -228,6 +234,14 @@ public class EntitiesFieldsAsyncLoaderTest { when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); doReturn(entityView).when(entityViewServiceMock).findEntityViewByIdAsync(eq(TENANT_ID), any()); + break; + case EDGE: + var edge = Futures.immediateFuture(entityDoesNotExist ? null : new Edge(new EdgeId(RANDOM_UUID))); + + when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); + doReturn(edge).when(edgeServiceMock).findEdgeByIdAsync(eq(TENANT_ID), any()); + break; default: throw new RuntimeException("Unexpected EntityType: " + entityType); @@ -252,6 +266,8 @@ public class EntitiesFieldsAsyncLoaderTest { return new RuleChain((RuleChainId) entityId); case ENTITY_VIEW: return new EntityView((EntityViewId) entityId); + case EDGE: + return new Edge((EdgeId) entityId); default: throw new RuntimeException("Unexpected EntityType: " + entityId.getEntityType()); } diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 688392d264..b6dc7248ab 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -155,6 +155,8 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" + # "" disables connection id support, 0 enables support but not for incoming traffic, any value greater than 0 set the connection id size in bytes + connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:6}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 805ab5a8dd..a40374e1f6 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -128,14 +128,18 @@ transport: bind_port: "${SNMP_BIND_PORT:1620}" response_processing: # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices - parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" + parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:4}" # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" - # Batch size to request OID mappings from the device (useful when the device profile has multiple hundreds of OID mappings) + # Maximum size of a PDU (amount of OID mappings in a single SNMP request). The request will be split into multiple PDUs if mappings amount exceeds this number max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" + # Delay after sending each request chunk (in case the request was split into multiple PDUs due to max_request_oids) + request_chunk_delay_ms: "${SNMP_REQUEST_CHUNK_DELAY_MS:100}" response: # To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types) ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" + # Thread pool size for scheduler that executes device querying tasks + scheduler_thread_pool_size: "${SNMP_SCHEDULER_THREAD_POOL_SIZE:4}" sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # The parameter value is in milliseconds. diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4bbe817fcf..ab60ebd525 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -44,6 +44,7 @@ "@ngrx/store-devtools": "^15.4.0", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", + "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.0", "@tinymce/tinymce-angular": "^7.0.0", "ace-builds": "1.4.13", diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 90d74a4c16..4b21a60b30 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -82,6 +82,7 @@ export interface RpcApi { export interface IWidgetUtils { formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; + getEntityDetailsPageURL: (id: string, entityType: EntityType) => string; } export interface WidgetActionsApi { diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 8df64be126..2339801e2b 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -18,7 +18,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; -import { Observable, of } from 'rxjs'; +import { Observable, of, ReplaySubject } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { NO_IMAGE_DATA_URI, @@ -36,6 +36,9 @@ import { ResourcesService } from '@core/services/resources.service'; providedIn: 'root' }) export class ImageService { + + private imagesLoading: { [url: string]: ReplaySubject } = {}; + constructor( private http: HttpClient, private sanitizer: DomSanitizer, @@ -95,12 +98,34 @@ export class ImageService { parts[parts.length - 1] = encodeURIComponent(key); const encodedUrl = parts.join('/'); const imageLink = preview ? (encodedUrl + '/preview') : encodedUrl; - const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); - return this.http - .get(imageLink, {...options, ...{ responseType: 'blob' } }).pipe( + return this.loadImageDataUrl(imageLink, asString, emptyUrl); + } + + private loadImageDataUrl(imageLink: string, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable { + let request: ReplaySubject; + if (this.imagesLoading[imageLink]) { + request = this.imagesLoading[imageLink]; + } else { + request = new ReplaySubject(1); + this.imagesLoading[imageLink] = request; + const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true}); + this.http.get(imageLink, {...options, ...{ responseType: 'blob' } }).subscribe({ + next: (value) => { + request.next(value); + request.complete(); + }, + error: err => { + request.error(err); + }, + complete: () => { + delete this.imagesLoading[imageLink]; + } + }); + } + return request.pipe( switchMap(val => blobToBase64(val).pipe( - map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl)) - )), + map((dataUrl) => asString ? dataUrl : this.sanitizer.bypassSecurityTrustUrl(dataUrl)) + )), catchError(() => of(asString ? emptyUrl : this.sanitizer.bypassSecurityTrustUrl(emptyUrl))) ); } diff --git a/ui-ngx/src/app/core/notification/notification.models.ts b/ui-ngx/src/app/core/notification/notification.models.ts index 1d658a5b9e..37e2177d62 100644 --- a/ui-ngx/src/app/core/notification/notification.models.ts +++ b/ui-ngx/src/app/core/notification/notification.models.ts @@ -33,6 +33,7 @@ export class NotificationMessage { horizontalPosition?: NotificationHorizontalPosition; verticalPosition?: NotificationVerticalPosition; panelClass?: string | string[]; + modern?: boolean; } export class HideNotification { diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index eeade82898..e1941590ea 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -27,7 +27,7 @@ import { widgetType } from '@shared/models/widget.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; -import { deepClone, isEqual } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils'; import { UtilsService } from '@core/services/utils.service'; import { Observable, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -309,6 +309,12 @@ export class ItemBufferService { if (origNode.error) { node.error = origNode.error; } + if (isDefinedAndNotNull(origNode.singletonMode)) { + node.singletonMode = origNode.singletonMode; + } + if (isDefinedAndNotNull(origNode.queueName)) { + node.queueName = origNode.queueName; + } ruleNodes.nodes.push(node); if (i === 0) { top = node.y; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index a12829f7c9..fcb4e0b411 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -194,6 +194,17 @@ import * as ScrollGridComponent from '@shared/components/grid/scroll-grid.compon import * as GalleryImageInputComponent from '@shared/components/image/gallery-image-input.component'; import * as MultipleGalleryImageInputComponent from '@shared/components/image/multiple-gallery-image-input.component'; +import * as CssUnitSelectComponent from '@home/components/widget/lib/settings/common/css-unit-select.component'; +import * as WidgetActionsPanelComponent from '@home/components/widget/config/basic/common/widget-actions-panel.component'; +import * as FontSettingsComponent from '@home/components/widget/lib/settings/common/font-settings.component'; +import * as ColorSettingsComponent from '@home/components/widget/lib/settings/common/color-settings.component'; +import * as DisplayColumnsPanelComponent from '@home/components/widget/lib/display-columns-panel.component'; +import * as AlarmDetailsDialogComponent from '@home/components/alarm/alarm-details-dialog.component'; +import * as AlarmAssigneePanelComponent from '@home/components/alarm/alarm-assignee-panel.component'; +import * as AlarmCommentDialogComponent from '@home/components/alarm/alarm-comment-dialog.component'; +import * as AlarmFilterConfigComponent from '@home/components/alarm/alarm-filter-config.component'; +import * as DatasourceComponent from '@home/components/widget/config/datasources.component'; +import * as DataKeysPanelComponent from '@home/components/widget/config/basic/common/data-keys-panel.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; import * as DetailsPanelComponent from '@home/components/details-panel.component'; @@ -239,6 +250,10 @@ import * as ImportDialogCsvComponent from '@shared/import-export/import-dialog-c import * as TableColumnsAssignmentComponent from '@shared/import-export/table-columns-assignment.component'; import * as EventContentDialogComponent from '@home/components/event/event-content-dialog.component'; import * as SharedHomeComponentsModule from '@home/components/shared-home-components.module'; +import * as WidgetConfigComponentsModule from '@home/components/widget/config/widget-config-components.module'; +import * as BasicWidgetConfigModule from '@home/components/widget/config/basic/basic-widget-config.module'; +import * as WidgetSettingsCommonModule from '@home/components/widget/lib/settings/common/widget-settings-common.module'; +import * as WidgetComponentsModule from '@home/components/widget/widget-components.module'; import * as SelectTargetLayoutDialogComponent from '@home/components/dashboard/select-target-layout-dialog.component'; import * as SelectTargetStateDialogComponent from '@home/components/dashboard/select-target-state-dialog.component'; import * as AliasesEntityAutocompleteComponent from '@home/components/alias/aliases-entity-autocomplete.component'; @@ -507,6 +522,17 @@ class ModulesMap implements IModulesMap { '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, + '@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent, + '@home/components/alarm/alarm-comment-dialog.component': AlarmCommentDialogComponent, + '@home/components/alarm/alarm-assignee-panel.component': AlarmAssigneePanelComponent, + '@home/components/alarm/alarm-details-dialog.component': AlarmDetailsDialogComponent, + '@home/components/widget/lib/display-columns-panel.component': DisplayColumnsPanelComponent, + '@home/components/widget/config/datasources.component': DatasourceComponent, + '@home/components/widget/config/basic/common/data-keys-panel.component': DataKeysPanelComponent, + '@home/components/widget/lib/settings/common/color-settings.component': ColorSettingsComponent, + '@home/components/widget/lib/settings/common/font-settings.component': FontSettingsComponent, + '@home/components/widget/config/basic/common/widget-actions-panel.component': WidgetActionsPanelComponent, + '@home/components/widget/lib/settings/common/css-unit-select.component': CssUnitSelectComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent, '@home/components/details-panel.component': DetailsPanelComponent, @@ -549,6 +575,10 @@ class ModulesMap implements IModulesMap { '@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent, '@home/components/event/event-content-dialog.component': EventContentDialogComponent, '@home/components/shared-home-components.module': SharedHomeComponentsModule, + '@home/components/widget/config/widget-config-components.module': WidgetConfigComponentsModule, + '@home/components/widget/config/basic/basic-widget-config.module': BasicWidgetConfigModule, + '@home/components/widget/lib/settings/common/widget-settings-common.module': WidgetSettingsCommonModule, + '@home/components/widget/widget-components.module': WidgetComponentsModule, '@home/components/dashboard/select-target-layout-dialog.component': SelectTargetLayoutDialogComponent, '@home/components/dashboard/select-target-state-dialog.component': SelectTargetStateDialogComponent, '@home/components/alias/aliases-entity-autocomplete.component': AliasesEntityAutocompleteComponent, diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 2d143f4e16..6cc1ec7dbe 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -214,7 +214,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo disableAutoPositionOnConflict: false, pushItems: false, swap: false, - maxRows: 100, + maxRows: 3000, minCols: this.columns ? this.columns : 24, maxCols: 3000, maxItemCols: 1000, @@ -293,12 +293,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } - if (updateMobileOpts) { - this.updateMobileOpts(); - } if (updateLayoutOpts) { this.updateLayoutOpts(); } + if (updateMobileOpts) { + this.updateMobileOpts(); + } if (updateEditingOpts) { this.updateEditingOpts(); } diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts index 080fd3e576..a37ae90dc0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts @@ -119,7 +119,7 @@ export class FiltersEditComponent implements OnInit, OnDestroy { const filteredArray = Object.entries(this.filtersInfo); if (filteredArray.length === 1) { - const singleFilter: Filter = {id: filteredArray[0][0], ...filteredArray[0][1]}; + const singleFilter: Filter = {id: filteredArray[0][0], ...deepClone(filteredArray[0][1])}; this.dialog.open(UserFilterDialogComponent, { disableClose: true, diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html index 1b1d97ae06..de80b9f238 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.html @@ -19,7 +19,7 @@
-
+
device-profile.snmp.scope diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss index 027e82c89c..e7877699b5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-communication-config.component.scss @@ -13,12 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../scss/constants'; + :host { .communication-config { border: 2px groove rgba(0, 0, 0, 0.25); border-radius: 4px; padding: 8px; min-width: 0; + flex-direction: column; + display: flex; + place-content: stretch flex-start; + align-items: stretch; + flex: 1; + gap: 0; } .scope-row { @@ -28,6 +36,13 @@ .required-text { margin: 16px 0 } + + @media #{$mat-gt-xmd} { + .communication-config { + flex-direction: row; + gap: 8px; + } + } } :host ::ng-deep { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html index f56bfa9255..8b4b656c02 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.html @@ -19,8 +19,8 @@
- - + +
@@ -28,7 +28,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss index d9ad3d7923..01ff5a71e2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-mapping.component.scss @@ -15,13 +15,9 @@ */ :host { .mapping-config { - min-width: 518px; - } - .mapping-list { - padding-bottom: 8px; - } - .required-text { - margin: 14px 0; + .required-text { + margin: 14px 0; + } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 4c309f5e80..867cd74f3b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -523,7 +523,7 @@ -
+
@@ -531,7 +531,7 @@ [type]="rateLimitsType.DEVICE_TELEMETRY_DATA_POINTS">
-
+
@@ -539,7 +539,7 @@ [type]="rateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION">
-
+
@@ -547,7 +547,7 @@ [type]="rateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT">
-
+
@@ -555,7 +555,7 @@ [type]="rateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION">
-
+
@@ -563,6 +563,14 @@ [type]="rateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT">
+
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index a0b8770509..0c95324d85 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -106,7 +106,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]], maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]], wsUpdatesPerSessionRateLimit: [null, []], - cassandraQueryTenantRateLimitsConfiguration: [null, []] + cassandraQueryTenantRateLimitsConfiguration: [null, []], + edgeEventRateLimits: [null, []], + edgeEventRateLimitsPerEdge: [null, []] }); this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts index 257752f5f4..8924682aa1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts @@ -35,7 +35,9 @@ export enum RateLimitsType { TENANT_ENTITY_EXPORT_RATE_LIMIT = 'TENANT_ENTITY_EXPORT_RATE_LIMIT', TENANT_ENTITY_IMPORT_RATE_LIMIT = 'TENANT_ENTITY_IMPORT_RATE_LIMIT', TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT', - TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT' + TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT', + EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT', + EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT' } export const rateLimitsLabelTranslationMap = new Map( @@ -54,6 +56,8 @@ export const rateLimitsLabelTranslationMap = new Map( [RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-import-rate-limit'], [RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.tenant-notification-request-rate-limit'], [RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.tenant-notification-requests-per-rule-rate-limit'], + [RateLimitsType.EDGE_EVENTS_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-rate-limit'], + [RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'], ] ); @@ -73,6 +77,8 @@ export const rateLimitsDialogTitleTranslationMap = new Map
- + -
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts index e65658ea53..2cdef0dea4 100644 --- a/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts @@ -34,6 +34,7 @@ import { selectHasRepository } from '@core/auth/auth.selectors'; import { catchError, mergeMap, take } from 'rxjs/operators'; import { of } from 'rxjs'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-repository-settings', @@ -48,6 +49,10 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit @Input() popoverComponent: TbPopoverComponent; + @Input() + @coerceBoolean() + hideLoadingBar = false; + repositorySettingsForm: UntypedFormGroup; settings: RepositorySettings = null; diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html index 4495ce5c5e..eb4b1d962b 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.component.html @@ -15,9 +15,11 @@ limitations under the License. --> - + *ngIf="!(hasRepository$ | async); else versionsTable"> + + +
+
widgets.power-button.behavior
+
+
widgets.rpc-state.initial-state
+ +
+
+
widgets.power-button.power-on
+ +
+
+
widgets.power-button.power-off
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + + + + +
+
+
+
{{ 'widgets.power-button.power-on-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.power-off-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.disabled-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts new file mode 100644 index 0000000000..174ded9ed3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts @@ -0,0 +1,195 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + powerButtonLayoutImages, + powerButtonLayouts, + powerButtonLayoutTranslations, + PowerButtonWidgetSettings +} from '@home/components/widget/lib/rpc/power-button-widget.models'; +import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-power-button-basic-config', + templateUrl: './power-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.powerButtonWidgetConfigForm.get('targetDevice').value; + } + + powerButtonLayouts = powerButtonLayouts; + + powerButtonLayoutTranslationMap = powerButtonLayoutTranslations; + powerButtonLayoutImageMap = powerButtonLayoutImages; + + valueType = ValueType; + + powerButtonWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.powerButtonWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: PowerButtonWidgetSettings = {...powerButtonDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.powerButtonWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + onUpdateState: [settings.onUpdateState, []], + offUpdateState: [settings.offUpdateState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + mainColorOn: [settings.mainColorOn, []], + backgroundColorOn: [settings.backgroundColorOn, []], + + mainColorOff: [settings.mainColorOff, []], + backgroundColorOff: [settings.backgroundColorOff, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.onUpdateState = config.onUpdateState; + this.widgetConfig.config.settings.offUpdateState = config.offUpdateState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + + this.widgetConfig.config.settings.layout = config.layout; + + this.widgetConfig.config.settings.mainColorOn = config.mainColorOn; + this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn; + + this.widgetConfig.config.settings.mainColorOff = config.mainColorOff; + this.widgetConfig.config.settings.backgroundColorOff = config.backgroundColorOff; + + this.widgetConfig.config.settings.mainColorDisabled = config.mainColorDisabled; + this.widgetConfig.config.settings.backgroundColorDisabled = config.backgroundColorDisabled; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value; + if (showTitle) { + this.powerButtonWidgetConfigForm.get('title').enable(); + this.powerButtonWidgetConfigForm.get('titleFont').enable(); + this.powerButtonWidgetConfigForm.get('titleColor').enable(); + this.powerButtonWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.powerButtonWidgetConfigForm.get('iconSize').enable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').enable(); + this.powerButtonWidgetConfigForm.get('icon').enable(); + this.powerButtonWidgetConfigForm.get('iconColor').enable(); + } else { + this.powerButtonWidgetConfigForm.get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('icon').disable(); + this.powerButtonWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.powerButtonWidgetConfigForm.get('title').disable(); + this.powerButtonWidgetConfigForm.get('titleFont').disable(); + this.powerButtonWidgetConfigForm.get('titleColor').disable(); + this.powerButtonWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.powerButtonWidgetConfigForm.get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('icon').disable(); + this.powerButtonWidgetConfigForm.get('iconColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html index 249acae52a..9f8d580bbc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html @@ -86,7 +86,7 @@
-
+
{{ 'widgets.progress-bar.value' | translate }} @@ -104,7 +104,7 @@
-
+
{{ 'widgets.progress-bar.range' | translate }}
widgets.progress-bar.min
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html index d79947185b..b00a89969b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html @@ -34,7 +34,7 @@ {{ 'widgets.liquid-level-card.title' | translate }} -
+
@@ -49,7 +49,7 @@ {{ 'widgets.liquid-level-card.icon' | translate }} -
+
@@ -64,7 +64,7 @@
-
+
widgets.liquid-level-card.shape
@@ -96,7 +96,7 @@ formControlName="shapeAttributeName">
-
+
widgets.liquid-level-card.shape-by-attribute
widgets.liquid-level-card.units
widgets.liquid-level-card.datasource-units
-
+
-
+
widgets.liquid-level-card.widget-units
-
+
@@ -154,9 +154,9 @@
-
+
widgets.liquid-level-card.total-volume
-
+
@@ -184,10 +184,30 @@ formControlName="volumeAttributeName"> - +
+
+
widgets.liquid-level-card.total-volume-units
+
+ + + + {{ DataSourceTypeTranslations.get(type) | translate }} + + + + + + + +
@@ -206,7 +226,7 @@
widgets.liquid-level-card.value
-
+
@@ -221,7 +241,7 @@
widgets.liquid-level-card.total-volume
-
+
@@ -247,7 +267,7 @@ {{ 'widgets.liquid-level-card.level' | translate }} -
+
@@ -268,7 +288,7 @@ {{ 'widgets.value-card.date' | translate }} -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts index b246ed2db1..0fc034d496 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.ts @@ -194,7 +194,9 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon volumeSource: [settings.volumeSource, []], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], + volumeUnitsSource: [settings.volumeUnitsSource, []], volumeUnits: [settings.volumeUnits, [Validators.required]], + volumeUnitsAttributeName: [settings.volumeUnitsAttributeName, [Validators.required]], volumeFont: [settings.volumeFont, []], volumeColor: [settings.volumeColor, []], units: [settings.units, [Validators.required]], @@ -260,6 +262,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.volumeSource = config.volumeSource; this.widgetConfig.config.settings.volumeConstant = config.volumeConstant; this.widgetConfig.config.settings.volumeAttributeName = config.volumeAttributeName; + this.widgetConfig.config.settings.volumeUnitsSource = config.volumeUnitsSource; + this.widgetConfig.config.settings.volumeUnitsAttributeName = config.volumeUnitsAttributeName; this.widgetConfig.config.settings.volumeUnits = config.volumeUnits; this.widgetConfig.config.settings.volumeFont = config.volumeFont; this.widgetConfig.config.settings.volumeColor = config.volumeColor; @@ -294,7 +298,7 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon protected validatorTriggers(): string[] { return [ 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', 'showTitleIcon', 'volumeSource', - 'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource' + 'showTooltipDate', 'layout', 'showTitle', 'widgetUnitsSource', 'volumeUnitsSource' ]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html index 18fa3b5587..c6ac3abb45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html @@ -82,7 +82,7 @@ {{ 'widgets.single-switch.auto-scale' | translate }}
-
+
{{ 'widgets.single-switch.label' | translate }} @@ -99,7 +99,7 @@
-
+
{{ 'widgets.single-switch.icon' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html new file mode 100644 index 0000000000..a8d84d4dc4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.html @@ -0,0 +1,271 @@ + + + +
+
widgets.slider.behavior
+
+
widgets.slider.initial-value
+ +
+
+
widgets.slider.on-value-change
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.slider.auto-scale' | translate }} + +
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.slider.icon' | translate }} + +
+ + + + + + + + +
+
+
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+
{{ 'widgets.slider.range' | translate }}
+
+
widgets.slider.min
+ + + +
widgets.slider.max
+ + + +
+
+
+ + {{ 'widgets.slider.range-ticks' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.slider.tick-marks' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.slider.colors' | translate }}
+
+
+
widgets.slider.main
+ + +
+ +
+
widgets.slider.background
+ + +
+
+
+
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
+
+
widgets.slider.main
+ + +
+ +
+
widgets.slider.background
+ + +
+
+
+
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
+
+
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts new file mode 100644 index 0000000000..b80c6acb08 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/slider-basic-config.component.ts @@ -0,0 +1,309 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + SliderLayout, + sliderLayoutImages, + sliderLayouts, + sliderLayoutTranslations, + sliderWidgetDefaultSettings, + SliderWidgetSettings +} from '@home/components/widget/lib/rpc/slider-widget.models'; +import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-slider-basic-config', + templateUrl: './slider-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class SliderBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.sliderWidgetConfigForm.get('targetDevice').value; + } + + sliderLayout = SliderLayout; + + sliderLayouts = sliderLayouts; + + sliderLayoutTranslationMap = sliderLayoutTranslations; + sliderLayoutImageMap = sliderLayoutImages; + + valueType = ValueType; + + sliderWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.sliderWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: SliderWidgetSettings = {...sliderWidgetDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.sliderWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + valueChange: [settings.valueChange, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + autoScale: [settings.autoScale, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + showValue: [settings.showValue, []], + valueUnits: [settings.valueUnits, []], + valueDecimals: [settings.valueDecimals, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + tickMin: [settings.tickMin, []], + tickMax: [settings.tickMax, []], + + showTicks: [settings.showTicks, []], + ticksFont: [settings.ticksFont, []], + ticksColor: [settings.ticksColor, []], + + showTickMarks: [settings.showTickMarks, []], + tickMarksCount: [settings.tickMarksCount, [Validators.min(2)]], + tickMarksColor: [settings.tickMarksColor, []], + + mainColor: [settings.mainColor, []], + backgroundColor: [settings.backgroundColor, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + leftIconSize: [settings.leftIconSize, [Validators.min(0)]], + leftIconSizeUnit: [settings.leftIconSizeUnit, []], + leftIcon: [settings.leftIcon, []], + leftIconColor: [settings.leftIconColor, []], + + rightIconSize: [settings.rightIconSize, [Validators.min(0)]], + rightIconSizeUnit: [settings.rightIconSizeUnit, []], + rightIcon: [settings.rightIcon, []], + rightIconColor: [settings.rightIconColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.valueChange = config.valueChange; + this.widgetConfig.config.settings.disabledState = config.disabledState; + + this.widgetConfig.config.settings.layout = config.layout; + this.widgetConfig.config.settings.autoScale = config.autoScale; + + this.widgetConfig.config.settings.showValue = config.showValue; + this.widgetConfig.config.settings.valueUnits = config.valueUnits; + this.widgetConfig.config.settings.valueDecimals = config.valueDecimals; + this.widgetConfig.config.settings.valueFont = config.valueFont; + this.widgetConfig.config.settings.valueColor = config.valueColor; + + this.widgetConfig.config.settings.tickMin = config.tickMin; + this.widgetConfig.config.settings.tickMax = config.tickMax; + + this.widgetConfig.config.settings.showTicks = config.showTicks; + this.widgetConfig.config.settings.ticksFont = config.ticksFont; + this.widgetConfig.config.settings.ticksColor = config.ticksColor; + + this.widgetConfig.config.settings.showTickMarks = config.showTickMarks; + this.widgetConfig.config.settings.tickMarksCount = config.tickMarksCount; + this.widgetConfig.config.settings.tickMarksColor = config.tickMarksColor; + + this.widgetConfig.config.settings.mainColor = config.mainColor; + this.widgetConfig.config.settings.backgroundColor = config.backgroundColor; + + this.widgetConfig.config.settings.mainColorDisabled = config.mainColorDisabled; + this.widgetConfig.config.settings.backgroundColorDisabled = config.backgroundColorDisabled; + + this.widgetConfig.config.settings.leftIconSize = config.leftIconSize; + this.widgetConfig.config.settings.leftIconSizeUnit = config.leftIconSizeUnit; + this.widgetConfig.config.settings.leftIcon = config.leftIcon; + this.widgetConfig.config.settings.leftIconColor = config.leftIconColor; + + this.widgetConfig.config.settings.rightIconSize = config.rightIconSize; + this.widgetConfig.config.settings.rightIconSizeUnit = config.rightIconSizeUnit; + this.widgetConfig.config.settings.rightIcon = config.rightIcon; + this.widgetConfig.config.settings.rightIconColor = config.rightIconColor; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showValue', 'showTicks', 'showTickMarks', 'layout']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.sliderWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.sliderWidgetConfigForm.get('showIcon').value; + const showValue: boolean = this.sliderWidgetConfigForm.get('showValue').value; + const showTicks: boolean = this.sliderWidgetConfigForm.get('showTicks').value; + const showTickMarks: boolean = this.sliderWidgetConfigForm.get('showTickMarks').value; + const layout: SliderLayout = this.sliderWidgetConfigForm.get('layout').value; + + const valueEnabled = layout !== SliderLayout.simplified; + const leftRightIconsEnabled = layout === SliderLayout.extended; + + if (showTitle) { + this.sliderWidgetConfigForm.get('title').enable(); + this.sliderWidgetConfigForm.get('titleFont').enable(); + this.sliderWidgetConfigForm.get('titleColor').enable(); + this.sliderWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.sliderWidgetConfigForm.get('iconSize').enable(); + this.sliderWidgetConfigForm.get('iconSizeUnit').enable(); + this.sliderWidgetConfigForm.get('icon').enable(); + this.sliderWidgetConfigForm.get('iconColor').enable(); + } else { + this.sliderWidgetConfigForm.get('iconSize').disable(); + this.sliderWidgetConfigForm.get('iconSizeUnit').disable(); + this.sliderWidgetConfigForm.get('icon').disable(); + this.sliderWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.sliderWidgetConfigForm.get('title').disable(); + this.sliderWidgetConfigForm.get('titleFont').disable(); + this.sliderWidgetConfigForm.get('titleColor').disable(); + this.sliderWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.sliderWidgetConfigForm.get('iconSize').disable(); + this.sliderWidgetConfigForm.get('iconSizeUnit').disable(); + this.sliderWidgetConfigForm.get('icon').disable(); + this.sliderWidgetConfigForm.get('iconColor').disable(); + } + + if (valueEnabled && showValue) { + this.sliderWidgetConfigForm.get('valueUnits').enable(); + this.sliderWidgetConfigForm.get('valueDecimals').enable(); + this.sliderWidgetConfigForm.get('valueFont').enable(); + this.sliderWidgetConfigForm.get('valueColor').enable(); + } else { + this.sliderWidgetConfigForm.get('valueUnits').disable(); + this.sliderWidgetConfigForm.get('valueDecimals').disable(); + this.sliderWidgetConfigForm.get('valueFont').disable(); + this.sliderWidgetConfigForm.get('valueColor').disable(); + } + + if (showTicks) { + this.sliderWidgetConfigForm.get('ticksFont').enable(); + this.sliderWidgetConfigForm.get('ticksColor').enable(); + } else { + this.sliderWidgetConfigForm.get('ticksFont').disable(); + this.sliderWidgetConfigForm.get('ticksColor').disable(); + } + + if (showTickMarks) { + this.sliderWidgetConfigForm.get('tickMarksCount').enable(); + this.sliderWidgetConfigForm.get('tickMarksColor').enable(); + } else { + this.sliderWidgetConfigForm.get('tickMarksCount').disable(); + this.sliderWidgetConfigForm.get('tickMarksColor').disable(); + } + + if (leftRightIconsEnabled) { + this.sliderWidgetConfigForm.get('leftIconSize').enable(); + this.sliderWidgetConfigForm.get('leftIconSizeUnit').enable(); + this.sliderWidgetConfigForm.get('leftIcon').enable(); + this.sliderWidgetConfigForm.get('leftIconColor').enable(); + this.sliderWidgetConfigForm.get('rightIconSize').enable(); + this.sliderWidgetConfigForm.get('rightIconSizeUnit').enable(); + this.sliderWidgetConfigForm.get('rightIcon').enable(); + this.sliderWidgetConfigForm.get('rightIconColor').enable(); + } else { + this.sliderWidgetConfigForm.get('leftIconSize').disable(); + this.sliderWidgetConfigForm.get('leftIconSizeUnit').disable(); + this.sliderWidgetConfigForm.get('leftIcon').disable(); + this.sliderWidgetConfigForm.get('leftIconColor').disable(); + this.sliderWidgetConfigForm.get('rightIconSize').disable(); + this.sliderWidgetConfigForm.get('rightIconSizeUnit').disable(); + this.sliderWidgetConfigForm.get('rightIcon').disable(); + this.sliderWidgetConfigForm.get('rightIconColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _valuePreviewFn(): string { + const units: string = this.sliderWidgetConfigForm.get('valueUnits').value; + const decimals: number = this.sliderWidgetConfigForm.get('valueDecimals').value; + return formatValue(48, decimals, units, false); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts index 26dc76866a..cac6928bac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts @@ -62,8 +62,6 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A loading$ = this.loadingSubject.asObservable().pipe(share()); - error = ''; - protected constructor(protected cd: ChangeDetectorRef) { } @@ -91,14 +89,15 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A ngOnDestroy() { this.valueActions.forEach(v => v.destroy()); + this.loadingSubject.complete(); + this.loadingSubject.unsubscribe(); } public onInit() { } public clearError() { - this.error = ''; - this.cd.markForCheck(); + this.ctx.hideToast(this.ctx.toastTargetId); } protected createValueGetter(getValueSettings: GetValueSettings, @@ -111,7 +110,8 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A } }, error: (err: any) => { - this.onError(err); + const message = parseError(this.ctx, err); + this.onError(message); if (valueObserver?.error) { valueObserver.error(err); } @@ -130,8 +130,7 @@ export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, A } private onError(error: string) { - this.error = error; - this.cd.markForCheck(); + this.ctx.showErrorToast(error, 'bottom', 'center', this.ctx.toastTargetId, true); } protected updateValue(valueSetter: ValueSetter, @@ -293,6 +292,8 @@ export class ValueToDataConverter { constructor(protected settings: ValueToDataSettings) { switch (settings.type) { + case ValueToDataType.VALUE: + break; case ValueToDataType.CONSTANT: this.constantValue = this.settings.constantValue; break; @@ -310,6 +311,8 @@ export class ValueToDataConverter { valueToData(value: V): any { switch (this.settings.type) { + case ValueToDataType.VALUE: + return value; case ValueToDataType.CONSTANT: return this.constantValue; case ValueToDataType.FUNCTION: diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss index 6641c2c32e..081cb00f0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss @@ -19,34 +19,3 @@ left: 0; right: 0; } -.tb-action-widget-error-container { - position: absolute; - bottom: 0; - left: 0; - right: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 1; - .tb-action-widget-error-panel { - display: flex; - padding: 4px 4px 4px 12px; - justify-content: center; - align-items: center; - gap: 4px; - border-radius: 4px; - background-color: #fff2f3; - box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2); - .tb-action-widget-error-text { - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 16px; - color: rgba(209, 39, 48, 1); - } - .tb-action-widget-error-clear { - color: rgba(209, 39, 48, 1); - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html index 02e2406859..294ba855fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html @@ -27,10 +27,4 @@ [ctx]="ctx" (clicked)="onClick($event)"> -
-
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html index 26f910fdd7..d48ba2f5e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/command-button-widget.component.html @@ -27,10 +27,4 @@ (clicked)="onClick($event)"> -
-
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts index af3bd41caa..25be87451e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -241,8 +241,8 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit } else { aggValue.value = 'N/A'; } + aggValue.color.update(value); const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals)); - aggValue.color.update(numeric); if (aggValue.showArrow && isDefined(numeric)) { aggValue.upArrow = numeric > 0; aggValue.downArrow = numeric < 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts index e43f22ff71..1c65920179 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts @@ -127,11 +127,11 @@ export class GatewayConfigurationComponent implements OnInit { type: [StorageTypes.MEMORY, [Validators.required]], read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], - data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]], + data_folder_path: ['./data/', [Validators.required]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], - data_file_path: ['./data/data.db', [Validators.pattern(/^[^\s]+$/)]], + data_file_path: ['./data/data.db', [Validators.required]], messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], @@ -240,7 +240,7 @@ export class GatewayConfigurationComponent implements OnInit { storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false}); storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false}); } else if (type === StorageTypes.FILE) { - storageGroup.get('data_folder_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]); + storageGroup.get('data_folder_path').addValidators([Validators.required]); storageGroup.get('max_file_count').addValidators( [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); storageGroup.get('max_read_records_count').addValidators( @@ -252,7 +252,7 @@ export class GatewayConfigurationComponent implements OnInit { storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false}); storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false}); } else if (type === StorageTypes.SQLITE) { - storageGroup.get('data_file_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]); + storageGroup.get('data_file_path').addValidators([Validators.required]); storageGroup.get('messages_ttl_check_in_hours').addValidators( [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); storageGroup.get('messages_ttl_in_days').addValidators( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 8182920805..2b9329dc21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -80,7 +80,7 @@ + [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px', textAlign: 'center'}"> {{ 'gateway.connectors-table-actions' | translate }} section:not(.table-section) { + max-width: unset; + @media #{$mat-gt-md} { + max-width: 50%; + } + } + .table-section { min-height: 35vh; overflow: hidden; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index a9465fc65b..8071a2f924 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -366,7 +366,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie type: 'success', duration: 1000, verticalPosition: 'top', - horizontalPosition: 'right', + horizontalPosition: 'left', target: 'dashboardRoot', forceDismiss: true })); @@ -381,6 +381,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if ($event) { $event.stopPropagation(); } + this.initialConnector = attribute.value; const title = `Delete connector ${attribute.key}?`; const content = `All connector data will be deleted.`; this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index 3c79883bf3..0933bfe537 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -106,6 +106,7 @@ export class LiquidLevelWidgetComponent implements OnInit { private volume: number; private tooltipContent: string; private widgetUnits: string; + private volumeUnits: string; private capacityUnits = Object.values(CapacityUnits); @@ -128,7 +129,7 @@ export class LiquidLevelWidgetComponent implements OnInit { this.getData().subscribe(data => { if (data) { - const { svg, volume, units } = data; + const { svg, volume, units, volumeUnits } = data; if (svg && isNotEmptyStr(svg) && this.liquidLevelContent.nativeElement) { const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); jQueryContainerElement.html(svg); @@ -145,6 +146,10 @@ export class LiquidLevelWidgetComponent implements OnInit { this.volume = Number(volume); } + if (volumeUnits) { + this.volumeUnits = volumeUnits; + } + if (units) { this.widgetUnits = units; } @@ -164,7 +169,7 @@ export class LiquidLevelWidgetComponent implements OnInit { this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); } - private getData(): Observable<{ svg: string; volume: number; units: string }> { + private getData(): Observable<{ svg: string; volume: number; units: string; volumeUnits: string}> { if (this.ctx.datasources?.length) { const entityId: EntityId = { entityType: this.ctx.datasources[0].entityType, @@ -308,7 +313,7 @@ export class LiquidLevelWidgetComponent implements OnInit { .pipe(map(attributes => { const shape = extractValue(attributes, this.settings.shapeAttributeName); if (!shape || !svgMapping.has(shape)) { - this.createdErrorMgs(this.settings.shapeAttributeName, isUndefinedOrNull(shape) || isEmptyStr(shape)); + this.createdErrorMsg(this.settings.shapeAttributeName, isUndefinedOrNull(shape) || isEmptyStr(shape)); return this.settings.selectedShape; } return shape; @@ -318,12 +323,15 @@ export class LiquidLevelWidgetComponent implements OnInit { return of(this.settings.selectedShape); } - private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string }> { + private getTankersParams(entityId: EntityId): Observable<{ volume: number; units: string; volumeUnits: string }> { const isVolumeStatic = this.settings.layout !== LevelCardLayout.absolute && this.settings.datasourceUnits === CapacityUnits.percent || this.settings.volumeSource === LiquidWidgetDataSourceType.static; const isUnitStatic = this.settings.layout !== LevelCardLayout.absolute || this.settings.widgetUnitsSource === LiquidWidgetDataSourceType.static; + const isVolumeUnitStatic = this.settings.layout !== LevelCardLayout.absolute + && this.settings.datasourceUnits === CapacityUnits.percent + || this.settings.volumeUnitsSource === LiquidWidgetDataSourceType.static; const attributeKeys: string[] = []; @@ -335,20 +343,29 @@ export class LiquidLevelWidgetComponent implements OnInit { attributeKeys.push(this.settings.widgetUnitsAttributeName); } + if (!isVolumeUnitStatic) { + attributeKeys.push(this.settings.volumeUnitsAttributeName); + } + if (!attributeKeys.length || entityId.id === NULL_UUID) { return of({ volume: this.settings.volumeConstant, + volumeUnits: this.settings.volumeUnits, units: this.settings.units }); } return this.ctx.attributeService.getEntityAttributes(entityId, null, attributeKeys).pipe( map(attributes => { - let volume = isVolumeStatic ? this.settings.volumeConstant : extractValue(attributes, this.settings.volumeAttributeName); - let units = isUnitStatic ? this.settings.units : extractValue(attributes, this.settings.widgetUnitsAttributeName); + let volume = isVolumeStatic ? this.settings.volumeConstant : + extractValue(attributes, this.settings.volumeAttributeName); + let volumeUnits = isVolumeUnitStatic ? this.settings.volumeUnits : + extractValue(attributes, this.settings.volumeUnitsAttributeName); + let units = isUnitStatic ? this.settings.units : + extractValue(attributes, this.settings.widgetUnitsAttributeName); if (!isVolumeStatic && (!volume || !isNumeric(volume) || volume < 0.1)) { - this.createdErrorMgs(this.settings.volumeAttributeName, isUndefinedOrNull(volume) || isEmptyStr(volume)); + this.createdErrorMsg(this.settings.volumeAttributeName, isUndefinedOrNull(volume) || isEmptyStr(volume)); volume = this.settings.volumeConstant; } @@ -358,20 +375,33 @@ export class LiquidLevelWidgetComponent implements OnInit { units = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits); } if (isUndefinedOrNull(units) || !isNotEmptyStr(units)) { - this.createdErrorMgs(this.settings.widgetUnitsAttributeName, isUndefinedOrNull(units) || isEmptyStr(units)); + this.createdErrorMsg(this.settings.widgetUnitsAttributeName, isUndefinedOrNull(units) || isEmptyStr(units)); units = this.settings.units; } } + if (!isVolumeUnitStatic) { + if (isNotEmptyStr(volumeUnits)) { + const normalizeUnits = volumeUnits.normalize().trim(); + volumeUnits = this.capacityUnits.find(unit => unit.normalize() === normalizeUnits); + } + if (isUndefinedOrNull(volumeUnits) || !isNotEmptyStr(volumeUnits)) { + this.createdErrorMsg(this.settings.widgetUnitsAttributeName, + isUndefinedOrNull(volumeUnits) || isEmptyStr(volumeUnits)); + volumeUnits = this.settings.volumeUnits; + } + } + return { volume, + volumeUnits, units }; }) ); } - private createdErrorMgs(attributeName: string, isEmpty = false) { + private createdErrorMsg(attributeName: string, isEmpty = false) { if (isEmpty) { this.errorsMsg.push(this.translate.instant('widgets.liquid-level-card.attribute-key-not-set', {attributeName})); } else { @@ -474,9 +504,14 @@ export class LiquidLevelWidgetComponent implements OnInit { } if (this.settings.layout === LevelCardLayout.absolute) { - const volumeInLiters: number = convertLiters(this.volume, this.settings.volumeUnits as CapacityUnits, ConversionType.to); - const volume = convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from) - .toFixed(this.settings.decimals || 0); + let volume: number | string; + if (this.widgetUnits !== CapacityUnits.percent) { + const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to); + volume = convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from) + .toFixed(this.settings.decimals || 0); + } else { + volume = this.volume.toFixed(this.settings.decimals || 0); + } const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont), color: this.settings.volumeColor}); @@ -553,7 +588,7 @@ export class LiquidLevelWidgetComponent implements OnInit { private convertInputData(value: any): number { if (this.settings.datasourceUnits !== CapacityUnits.percent) { return (convertLiters(Number(value), this.settings.datasourceUnits, ConversionType.to) / - convertLiters(this.volume, this.settings.volumeUnits, ConversionType.to)) * 100; + convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to)) * 100; } return Number(value); @@ -561,7 +596,7 @@ export class LiquidLevelWidgetComponent implements OnInit { private convertOutputData(value: number): number { if (this.widgetUnits !== CapacityUnits.percent) { - return convertLiters(this.volume * (value / 100), this.settings.volumeUnits, ConversionType.to); + return convertLiters(this.volume * (value / 100), this.volumeUnits as CapacityUnits, ConversionType.to); } return value; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts index 954e228e79..632cfe570d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.models.ts @@ -56,6 +56,8 @@ export interface LevelCardWidgetSettings extends WidgetConfig { volumeSource: LiquidWidgetDataSourceType; volumeConstant: number; volumeAttributeName: string; + volumeUnitsSource: LiquidWidgetDataSourceType; + volumeUnitsAttributeName: string; volumeUnits: CapacityUnits; volumeFont: Font; volumeColor: string; @@ -257,8 +259,10 @@ export const levelCardDefaultSettings: LevelCardWidgetSettings = { iconColor: '#5469FF', volumeSource: LiquidWidgetDataSourceType.static, volumeConstant: 500, - volumeUnits: CapacityUnits.liters, volumeAttributeName: 'volume', + volumeUnitsSource: LiquidWidgetDataSourceType.static, + volumeUnitsAttributeName: 'volumeUnits', + volumeUnits: CapacityUnits.liters, volumeFont: { family: 'Roboto', size: 14, @@ -375,9 +379,7 @@ export const convertLiters = (value: number, units: CapacityUnits, conversionTyp return conversionType === ConversionType.to ? value / factor : value * factor; }; -export const extractValue = (attributes: Array, attributeName: string): T | undefined => { - return attributes.find(attr => attr.key === attributeName)?.value; -}; +export const extractValue = (attributes: Array, attributeName: string): T | undefined => attributes.find(attr => attr.key === attributeName)?.value; export const valueContainerStyleDefaults = cssTextFromInlineStyle({ width: '100%', @@ -494,6 +496,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { const datasourceUnits: string = formGroup.get('datasourceUnits').value; const layout: LevelCardLayout = formGroup.get('layout').value; const volumeSource: LiquidWidgetDataSourceType = formGroup.get('volumeSource').value; + const volumeUnitsSource: LiquidWidgetDataSourceType = formGroup.get('volumeUnitsSource').value; const widgetUnitsSource: LiquidWidgetDataSourceType = formGroup.get('widgetUnitsSource').value; const showTooltipLevel: boolean = formGroup.get('showTooltipLevel').value; const showTooltipDate: boolean = formGroup.get('showTooltipDate').value; @@ -517,7 +520,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { if (datasourceUnits !== CapacityUnits.percent) { formGroup.get('volumeSource').enable({emitEvent: false}); - formGroup.get('volumeUnits').enable({emitEvent: false}); + formGroup.get('volumeUnitsSource').enable({emitEvent: false}); if (volumeSource === LiquidWidgetDataSourceType.static) { formGroup.get('volumeConstant').enable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false}); @@ -525,11 +528,20 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeAttributeName').enable({emitEvent: false}); } + if (volumeUnitsSource === LiquidWidgetDataSourceType.static) { + formGroup.get('volumeUnits').enable({emitEvent: false}); + formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false}); + } else { + formGroup.get('volumeUnits').disable({emitEvent: false}); + formGroup.get('volumeUnitsAttributeName').enable({emitEvent: false}); + } } else { formGroup.get('volumeSource').disable({emitEvent: false}); formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false}); + formGroup.get('volumeUnitsSource').disable({emitEvent: false}); formGroup.get('volumeUnits').disable({emitEvent: false}); + formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false}); } if (layout === LevelCardLayout.simple) { @@ -557,7 +569,7 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { } formGroup.get('volumeSource').enable({emitEvent: false}); - formGroup.get('volumeUnits').enable({emitEvent: false}); + formGroup.get('volumeUnitsSource').enable({emitEvent: false}); if (volumeSource === LiquidWidgetDataSourceType.static) { formGroup.get('volumeConstant').enable({emitEvent: false}); formGroup.get('volumeAttributeName').disable({emitEvent: false}); @@ -565,6 +577,13 @@ export const updatedFormSettingsValidators = (formGroup: FormGroup) => { formGroup.get('volumeConstant').disable({emitEvent: false}); formGroup.get('volumeAttributeName').enable({emitEvent: false}); } + if (volumeUnitsSource === LiquidWidgetDataSourceType.static) { + formGroup.get('volumeUnits').enable({emitEvent: false}); + formGroup.get('volumeUnitsAttributeName').disable({emitEvent: false}); + } else { + formGroup.get('volumeUnits').disable({emitEvent: false}); + formGroup.get('volumeUnitsAttributeName').enable({emitEvent: false}); + } if (formGroup.get('decimals')) { formGroup.get('decimals').enable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 8b20b8d93f..5540c1351a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -32,7 +32,7 @@ import { WidgetUnitedMapSettings } from './map-models'; import { Marker } from './markers'; -import { map, Observable, of, switchMap } from 'rxjs'; +import { map, Observable, of } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { Circle } from './circle'; @@ -64,6 +64,7 @@ import { MatDialog } from '@angular/material/dialog'; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { take, tap } from 'rxjs/operators'; export default abstract class LeafletMap { @@ -940,7 +941,12 @@ export default abstract class LeafletMap { this.markersData = markersData; if (this.options.useClusterMarkers) { if (createdMarkers.length) { - this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); + createdMarkers.forEach((marker) => { + marker.createMarkerIconSubject.pipe( + tap(() => this.markersCluster.addLayer(marker.leafletMarker)), + take(1) + ).subscribe(); + }); } if (updatedMarkers.length) { this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker)); @@ -971,10 +977,15 @@ export default abstract class LeafletMap { } this.markers.set(key, newMarker); if (!this.options.useClusterMarkers) { - this.map.addLayer(newMarker.leafletMarker); - if (this.map.pm.globalDragModeEnabled() && newMarker.leafletMarker.pm) { - newMarker.leafletMarker.pm.enableLayerDrag(); - } + newMarker.createMarkerIconSubject.pipe( + tap(() => { + this.map.addLayer(newMarker.leafletMarker); + if (this.map.pm.globalDragModeEnabled() && newMarker.leafletMarker.pm) { + newMarker.leafletMarker.pm.enableLayerDrag(); + } + }), + take(1) + ).subscribe(); } return newMarker; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 23913f75da..582faf6ebd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -23,6 +23,7 @@ import { fillDataPattern, isDefined, isDefinedAndNotNull, processDataPattern, sa import LeafletMap from './leaflet-map'; import { FormattedData } from '@shared/models/widget.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { ReplaySubject } from 'rxjs'; export class Marker { @@ -33,6 +34,7 @@ export class Marker { tooltipOffset: L.LatLngTuple; markerOffset: L.LatLngTuple; tooltip: L.Popup; + createMarkerIconSubject = new ReplaySubject(); constructor(private map: LeafletMap, private location: L.LatLng, @@ -148,6 +150,7 @@ export class Marker { this.labelOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10]; } this.updateMarkerLabel(settings); + this.createMarkerIconSubject.next(iconInfo); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html new file mode 100644 index 0000000000..48e4106a97 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.html @@ -0,0 +1,26 @@ + +
+
+ +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss new file mode 100644 index 0000000000..d65341a7ba --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-power-button-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-power-button-overlay) { + z-index: 1; + } + .tb-power-button-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-power-button-content { + flex: 1; + min-width: 0; + min-height: 0; + .tb-power-button-shape { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + svg { + .tb-small-shadow { + filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.2)); + } + .tb-shadow { + filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.15)); + } + } + &.tb-power-button-pointer { + svg { + .tb-hover-circle { + cursor: pointer; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts new file mode 100644 index 0000000000..02c58b3e9a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts @@ -0,0 +1,203 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { Observable } from 'rxjs'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + PowerButtonShape, + powerButtonShapeSize, + PowerButtonWidgetSettings +} from '@home/components/widget/lib/rpc/power-button-widget.models'; +import { SVG, Svg } from '@svgdotjs/svg.js'; + +@Component({ + selector: 'tb-power-button-widget', + templateUrl: './power-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './power-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class PowerButtonWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('powerButtonShape', {static: false}) + powerButtonShape: ElementRef; + + settings: PowerButtonWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + value = false; + disabled = false; + + private shapeResize$: ResizeObserver; + private drawSvgShapePending = false; + private svgShape: Svg; + private powerButtonSvgShape: PowerButtonShape; + private disabledState = false; + + private onValueSetter: ValueSetter; + private offValueSetter: ValueSetter; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + protected cd: ChangeDetectorRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...powerButtonDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onValue(value) + }); + + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onDisabled(value) + }); + + const onUpdateStateSettings = {...this.settings.onUpdateState, + actionLabel: this.ctx.translate.instant('widgets.power-button.power-on')}; + this.onValueSetter = this.createValueSetter(onUpdateStateSettings); + + const offUpdateStateSettings = {...this.settings.offUpdateState, + actionLabel: this.ctx.translate.instant('widgets.power-button.power-off')}; + this.offValueSetter = this.createValueSetter(offUpdateStateSettings); + + this.loading$.subscribe((loading) => { + this.updateDisabledState(loading || this.disabled); + this.cd.markForCheck(); + }); + } + + ngAfterViewInit(): void { + if (this.drawSvgShapePending) { + this.drawSvg(); + } + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + if (this.powerButtonShape) { + this.drawSvg(); + } else { + this.drawSvgShapePending = true; + } + this.cd.detectChanges(); + } + + private onValue(value: boolean): void { + const newValue = !!value; + if (this.value !== newValue) { + this.value = newValue; + this.powerButtonSvgShape?.setValue(this.value); + this.cd.markForCheck(); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.disabled = newDisabled; + this.updateDisabledState(this.disabled); + this.cd.markForCheck(); + } + } + + private onClick() { + if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) { + this.onValue(!this.value); + const targetValue = this.value; + const targetSetter = targetValue ? this.onValueSetter : this.offValueSetter; + this.powerButtonSvgShape?.setPressed(true); + this.updateValue(targetSetter, targetValue, { + next: () => { + this.powerButtonSvgShape?.setPressed(false); + this.onValue(targetValue); + }, + error: () => { + this.powerButtonSvgShape?.setPressed(false); + this.onValue(!targetValue); + } + }); + } + } + + private drawSvg() { + this.svgShape = SVG().addTo(this.powerButtonShape.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize); + this.renderer.setStyle(this.svgShape.node, 'overflow', 'visible'); + this.renderer.setStyle(this.svgShape.node, 'user-select', 'none'); + + this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, + this.settings, this.value, this.disabledState, () => this.onClick()); + + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.powerButtonShape.nativeElement); + this.onResize(); + } + + private updateDisabledState(disabled: boolean) { + this.disabledState = disabled; + this.powerButtonSvgShape?.setDisabled(this.disabledState); + } + + private onResize() { + const shapeWidth = this.powerButtonShape.nativeElement.getBoundingClientRect().width; + const shapeHeight = this.powerButtonShape.nativeElement.getBoundingClientRect().height; + const size = Math.min(shapeWidth, shapeHeight); + const scale = size / powerButtonShapeSize; + this.renderer.setStyle(this.svgShape.node, 'transform', `scale(${scale})`); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts new file mode 100644 index 0000000000..2e8ca0de6d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -0,0 +1,953 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { + DataToValueType, + GetValueAction, + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { Circle, Effect, Element, G, Gradient, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js'; +import '@svgdotjs/svg.filter.js'; +import tinycolor from 'tinycolor2'; +import { WidgetContext } from '@home/models/widget-component.models'; + +export enum PowerButtonLayout { + default = 'default', + simplified = 'simplified', + outlined = 'outlined', + default_volume = 'default_volume', + simplified_volume = 'simplified_volume', + outlined_volume = 'outlined_volume' +} + +export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[]; + +export const powerButtonLayoutTranslations = new Map( + [ + [PowerButtonLayout.default, 'widgets.power-button.layout-default'], + [PowerButtonLayout.simplified, 'widgets.power-button.layout-simplified'], + [PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'], + [PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'], + [PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'], + [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'] + ] +); + +export const powerButtonLayoutImages = new Map( + [ + [PowerButtonLayout.default, 'assets/widget/power-button/default-layout.svg'], + [PowerButtonLayout.simplified, 'assets/widget/power-button/simplified-layout.svg'], + [PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'], + [PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'], + [PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'], + [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'] + ] +); + +export interface PowerButtonWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + onUpdateState: SetValueSettings; + offUpdateState: SetValueSettings; + layout: PowerButtonLayout; + mainColorOn: string; + backgroundColorOn: string; + mainColorOff: string; + backgroundColorOff: string; + mainColorDisabled: string; + backgroundColorDisabled: string; + background: BackgroundSettings; +} + +export const powerButtonDefaultSettings: PowerButtonWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: false, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + onUpdateState: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SHARED_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.CONSTANT, + constantValue: true, + valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' + } + }, + offUpdateState: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SHARED_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.CONSTANT, + constantValue: false, + valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;' + } + }, + layout: PowerButtonLayout.default, + mainColorOn: '#3F52DD', + backgroundColorOn: '#FFFFFF', + mainColorOff: '#A2A2A2', + backgroundColorOff: '#FFFFFF', + mainColorDisabled: 'rgba(0,0,0,0.12)', + backgroundColorDisabled: '#FFFFFF', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; + +interface PowerButtonColor { + hex: string; + opacity: number; +} + +type PowerButtonState = 'on' | 'off' | 'disabled'; + +interface PowerButtonColorState { + mainColor: PowerButtonColor; + backgroundColor: PowerButtonColor; +} + +type PowerButtonShapeColors = Record; + +const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => { + const mainColorOn = tinycolor(settings.mainColorOn); + const backgroundColorOn = tinycolor(settings.backgroundColorOn); + const mainColorOff = tinycolor(settings.mainColorOff); + const backgroundColorOff = tinycolor(settings.backgroundColorOff); + const mainColorDisabled = tinycolor(settings.mainColorDisabled); + const backgroundColorDisabled = tinycolor(settings.backgroundColorDisabled); + return { + on: { + mainColor: {hex: mainColorOn.toHexString(), opacity: mainColorOn.getAlpha()}, + backgroundColor: {hex: backgroundColorOn.toHexString(), opacity: backgroundColorOn.getAlpha()}, + }, + off: { + mainColor: {hex: mainColorOff.toHexString(), opacity: mainColorOff.getAlpha()}, + backgroundColor: {hex: backgroundColorOff.toHexString(), opacity: backgroundColorOff.getAlpha()}, + }, + disabled: { + mainColor: {hex: mainColorDisabled.toHexString(), opacity: mainColorDisabled.getAlpha()}, + backgroundColor: {hex: backgroundColorDisabled.toHexString(), opacity: backgroundColorDisabled.getAlpha()}, + } + }; +}; + +export const powerButtonShapeSize = 110; +const cx = powerButtonShapeSize / 2; +const cy = powerButtonShapeSize / 2; + +const powerButtonAnimation = (element: Element): Runner => element.animate(200, 0, 'now'); + +export abstract class PowerButtonShape { + + static fromSettings(ctx: WidgetContext, + svgShape: Svg, + settings: PowerButtonWidgetSettings, + value: boolean, + disabled: boolean, + onClick: () => void): PowerButtonShape { + switch (settings.layout) { + case PowerButtonLayout.default: + return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.simplified: + return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.outlined: + return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.default_volume: + return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.simplified_volume: + return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.outlined_volume: + return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + } + } + + protected readonly colors: PowerButtonShapeColors; + protected readonly onLabel: string; + protected readonly offLabel: string; + + protected backgroundShape: Circle; + protected hoverShape: Circle; + protected hovered = false; + protected pressed = false; + protected forcePressed = false; + + protected constructor(protected widgetContext: WidgetContext, + protected svgShape: Svg, + protected settings: PowerButtonWidgetSettings, + protected value: boolean, + protected disabled: boolean, + protected onClick: () => void) { + this.colors = createPowerButtonShapeColors(this.settings); + this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase(); + this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase(); + this._drawShape(); + } + + public setValue(value: boolean) { + if (this.value !== value) { + this.value = value; + this._drawState(); + } + } + + public setDisabled(disabled: boolean) { + if (this.disabled !== disabled) { + this.disabled = disabled; + this._drawState(); + } + } + + public setPressed(pressed: boolean) { + if (this.forcePressed !== pressed) { + this.forcePressed = pressed; + if (this.forcePressed && !this.pressed) { + this.onPressStart(); + } else if (!this.forcePressed && !this.pressed) { + this.onPressEnd(); + } + } + } + + private _drawShape() { + + this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + + this.drawShape(); + + this.hoverShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy).addClass('tb-hover-circle') + .fill({color: '#000000', opacity: 0}); + this.hoverShape.on('mouseover', () => { + this.hovered = true; + if (!this.disabled) { + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0.06}); + } + }); + this.hoverShape.on('mouseout', () => { + this.hovered = false; + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0}); + this._cancelPressed(); + }); + this.hoverShape.on('touchmove', (event: TouchEvent) => { + const touch = event.touches[0]; + const element = document.elementFromPoint(touch.pageX,touch.pageY); + if (this.hoverShape.node !== element) { + this._cancelPressed(); + } + }); + this.hoverShape.on('touchcancel', () => { + this._cancelPressed(); + }); + this.hoverShape.on('mousedown touchstart', (event: Event) => { + if (event.type === 'mousedown') { + if ((event as MouseEvent).button !== 0) { + return; + } + } + if (!this.disabled && !this.pressed) { + this.pressed = true; + if (!this.forcePressed) { + this.onPressStart(); + } + } + }); + this.hoverShape.on('mouseup touchend touchcancel', () => { + if (this.pressed && !this.disabled) { + this.onClick(); + } + this._cancelPressed(); + }); + this._drawState(); + } + + private _cancelPressed() { + if (this.pressed) { + this.pressed = false; + if (!this.forcePressed) { + this.onPressEnd(); + } + } + } + + private _drawState() { + let colorState: PowerButtonColorState; + if (this.disabled) { + colorState = this.colors.disabled; + } else { + colorState = this.value ? this.colors.on : this.colors.off; + } + this.drawBackgroundState(colorState.backgroundColor); + this.drawColorState(colorState.mainColor); + if (this.value) { + this.drawOn(); + } else { + this.drawOff(); + } + if (this.disabled) { + this.hoverShape.timeline().finish(); + this.hoverShape.attr({'fill-opacity': 0}); + } else if (this.hovered) { + this.hoverShape.timeline().finish(); + this.hoverShape.animate(200).attr({'fill-opacity': 0.06}); + } + } + + private drawBackgroundState(backgroundColor: PowerButtonColor) { + this.backgroundShape.attr({ fill: backgroundColor.hex, 'fill-opacity': backgroundColor.opacity}); + } + + protected drawShape() {} + + protected drawColorState(_mainColor: PowerButtonColor) {} + + protected drawOff() {} + + protected drawOn() {} + + protected onPressStart() {} + + protected onPressEnd() {} + + protected createMask(shape: Element, maskElements: Element[]) { + const mask = + this.svgShape.mask().add(this.svgShape.rect().width('100%').height('100%').fill('#fff')); + maskElements.forEach(e => { + mask.add(e.fill('#000').attr({'fill-opacity': 1})); + }); + shape.maskWith(mask); + } + + protected createOnLabel(fontWeight = '500'): Text { + return this.createLabel(this.onLabel, fontWeight); + } + + protected createOffLabel(fontWeight = '500'): Text { + return this.createLabel(this.offLabel, fontWeight); + } + + private createLabel(text: string, fontWeight = '500'): Text { + return this.svgShape.text(text).font({ + family: 'Roboto', + weight: fontWeight, + style: 'normal', + size: '22px' + }).attr({x: '50%', y: '50%', 'text-anchor': 'middle', 'dominant-baseline': 'middle'}); + } + +} + +class InnerShadowCircle { + + private shadowCircle: Circle; + private blurEffect: Effect; + private offsetEffect: Effect; + private floodEffect: Effect; + + constructor(private svgShape: Svg, + private diameter: number, + private centerX: number, + private centerY: number, + private blur = 6, + private shadowOpacity = 0.6, + private dx = 0, + private dy = 0, + private shadowColor = '#000') { + + this.shadowCircle = this.svgShape.circle(this.diameter).center(this.centerX, this.centerY) + .fill({color: '#fff', opacity: 1}).stroke({width: 0}); + + this.shadowCircle.filterWith(add => { + add.x('-50%').y('-50%').width('200%').height('200%'); + let effect: Effect = add.componentTransfer(components => { + components.funcA({ type: 'table', tableValues: '1 0' }); + }).in(add.$fill); + effect = effect.gaussianBlur(this.blur, this.blur).attr({stdDeviation: this.blur}); + this.blurEffect = effect; + effect = effect.offset(this.dx, this.dy); + this.offsetEffect = effect; + effect = effect.flood(this.shadowColor, this.shadowOpacity); + this.floodEffect = effect; + effect = effect.composite(this.offsetEffect, 'in'); + effect.composite(add.$sourceAlpha, 'in'); + add.merge(m => { + m.mergeNode(add.$fill); + m.mergeNode(); + }); + }); + } + + public timeline(tl: Timeline): void { + this.blurEffect.timeline(tl); + this.offsetEffect.timeline(tl); + this.floodEffect.timeline(tl); + } + + public animate(blur: number, opacity: number, dx = 0, dy = 0): Runner { + powerButtonAnimation(this.blurEffect).attr({stdDeviation: blur}); + powerButtonAnimation(this.offsetEffect).attr({dx, dy}); + return powerButtonAnimation(this.floodEffect).attr({'flood-opacity': opacity}); + } + + public animateRestore(): Runner { + return this.animate(this.blur, this.shadowOpacity, this.dx, this.dy); + } + + public show(): void { + this.shadowCircle.show(); + } + + public hide(): void { + this.shadowCircle.hide(); + } + +} + +class DefaultPowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: InnerShadowCircle; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20) + .center(cx, cy); + this.onLabelShape = this.createOnLabel(); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 20, cx, cy, 0, 0); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor) { + this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.outerBorderMask.radius((powerButtonShapeSize - 20)/2); + this.onCircleShape.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.outerBorderMask.radius((powerButtonShapeSize - 2)/2); + this.centerGroup.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.pressedShadow.animate(6, 0.6); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedShadow.animateRestore(); + } + +} + +class SimplifiedPowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private onCircleShape: Circle; + private offLabelShape: Text; + private onLabelShape: Text; + private pressedShadow: InnerShadowCircle; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy); + this.onLabelShape = this.createOnLabel(); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor) { + this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCircleShape.hide(); + this.outerBorder.show(); + this.centerGroup.show(); + } + + protected drawOn() { + this.centerGroup.hide(); + this.outerBorder.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.pressedShadow.animate(6, 0.6); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedShadow.animateRestore(); + } +} + +class OutlinedPowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private innerBorder: Circle; + private innerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: InnerShadowCircle; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 2).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.createMask(this.innerBorder, [this.innerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.onCenterGroup = this.svgShape.group(); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy) + .addTo(this.onCenterGroup); + this.onLabelShape = this.createOnLabel(); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 0, 0); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor) { + this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.innerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCenterGroup.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.centerGroup.hide(); + this.onCenterGroup.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98}); + this.pressedShadow.animate(6, 0.6); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); + powerButtonAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedShadow.animateRestore(); + } +} + +class DefaultVolumePowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private outerBorderGradient: Gradient; + private innerBorder: Circle; + private innerBorderMask: Circle; + private innerBorderGradient: Gradient; + private innerShadow: InnerShadowCircle; + //private innerShadowGradient: Gradient; + //private innerShadowGradientStop: Stop; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedTimeline: Timeline; + private centerGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.outerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.268, 0.92).to(0.832, 0.1188); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.createMask(this.innerBorder, [this.innerBorderMask]); + this.innerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.832, 0.1188).to(0.268, 0.92); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); + this.onLabelShape = this.createOnLabel('400'); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.innerShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + if (this.disabled) { + this.backgroundShape.removeClass('tb-small-shadow'); + if (!this.forcePressed) { + this.innerShadow.hide(); + } + this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.backgroundShape.addClass('tb-small-shadow'); + this.innerShadow.show(); + this.outerBorder.fill(this.outerBorderGradient); + this.outerBorder.attr({ 'fill-opacity': 1 }); + this.innerBorder.fill(this.innerBorderGradient); + this.innerBorder.attr({ 'fill-opacity': 1 }); + } + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCircleShape.hide(); + this.centerGroup.show(); + this.innerBorder.show(); + } + + protected drawOn() { + if (this.disabled) { + this.innerBorder.hide(); + } else { + this.innerBorder.show(); + } + this.centerGroup.hide(); + this.onCircleShape.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + this.innerShadow.show(); + const pressedScale = 0.75; + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale}); + this.innerShadow.animate(6, 0.6); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onLabelShape).transform({scale: 1}); + this.innerShadow.animateRestore().after(() => { + if (this.disabled) { + this.innerShadow.hide(); + } + }); + } + +} + +class SimplifiedVolumePowerButtonShape extends PowerButtonShape { + + private outerBorder: Circle; + private outerBorderMask: Circle; + private offLabelShape: Text; + private onLabelShape: Text; + private innerShadow: InnerShadowCircle; + private pressedShadow: InnerShadowCircle; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({color: '#FAFAFA', opacity: 1}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.onCenterGroup = this.svgShape.group(); + this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); + this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3); + this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + if (!this.pressed) { + this.backgroundShape.addClass('tb-shadow'); + } + this.innerShadow.hide(); + this.onCenterGroup.hide(); + this.centerGroup.show(); + } + + protected drawOn() { + this.backgroundShape.removeClass('tb-shadow'); + this.centerGroup.hide(); + this.onCenterGroup.show(); + this.innerShadow.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + if (!this.value) { + this.backgroundShape.removeClass('tb-shadow'); + } + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onCenterGroup).transform({scale: pressedScale}); + this.pressedShadow.animate(8, 0.4); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); + this.pressedShadow.animateRestore().after(() => { + if (!this.value) { + this.backgroundShape.addClass('tb-shadow'); + } + }); + } +} + +class OutlinedVolumePowerButtonShape extends PowerButtonShape { + private outerBorder: Circle; + private outerBorderMask: Circle; + private outerBorderGradient: Gradient; + private innerBorder: Circle; + private innerBorderMask: Circle; + private offLabelShape: Text; + private onCircleShape: Circle; + private onLabelShape: Text; + private pressedShadow: InnerShadowCircle; + private pressedTimeline: Timeline; + private centerGroup: G; + private onCenterGroup: G; + + protected drawShape() { + this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.createMask(this.outerBorder, [this.outerBorderMask]); + this.outerBorderGradient = this.svgShape.gradient('linear', (add) => { + add.stop(0, '#CCCCCC', 1); + add.stop(1, '#FFFFFF', 1); + }).from(0.268, 0.92).to(0.832, 0.1188); + this.innerBorder = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy) + .fill({opacity: 0}).stroke({width: 0}); + this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy); + this.createMask(this.innerBorder, [this.innerBorderMask]); + this.centerGroup = this.svgShape.group(); + this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); + this.onCenterGroup = this.svgShape.group(); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) + .addTo(this.onCenterGroup); + this.onLabelShape = this.createOnLabel('800'); + this.createMask(this.onCircleShape, [this.onLabelShape]); + this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); + this.backgroundShape.addClass('tb-small-shadow'); + + this.pressedTimeline = new Timeline(); + this.centerGroup.timeline(this.pressedTimeline); + this.onCenterGroup.timeline(this.pressedTimeline); + this.onLabelShape.timeline(this.pressedTimeline); + this.pressedShadow.timeline(this.pressedTimeline); + } + + protected drawColorState(mainColor: PowerButtonColor){ + if (this.disabled) { + this.outerBorder.attr({ fill: '#000000', 'fill-opacity': 0.03}); + } else { + this.outerBorder.fill(this.outerBorderGradient); + this.outerBorder.attr({ 'fill-opacity': 1 }); + } + this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected drawOff() { + this.onCenterGroup.hide(); + this.centerGroup.show(); + this.innerBorder.show(); + } + + protected drawOn() { + this.innerBorder.hide(); + this.centerGroup.hide(); + this.onCenterGroup.show(); + } + + protected onPressStart() { + this.pressedTimeline.finish(); + const pressedScale = 0.75; + powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); + powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98}); + this.pressedShadow.animate(6, 0.6); + } + + protected onPressEnd() { + this.pressedTimeline.finish(); + powerButtonAnimation(this.centerGroup).transform({scale: 1}); + powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); + powerButtonAnimation(this.onLabelShape).transform({scale: 1}); + this.pressedShadow.animateRestore(); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html index c1c05cf880..fd9aaca4a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html @@ -37,10 +37,4 @@
-
-
-
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html new file mode 100644 index 0000000000..f15892dd52 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.html @@ -0,0 +1,59 @@ + +
+
+
+ +
+
+
+
{{ valueText }}
+
+
+
+ {{ leftIcon }} +
+
+ + + +
+
+
{{ settings.tickMin }}
+
+
+
{{ settings.tickMax }}
+
+
+
+
+ {{ rightIcon }} +
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss new file mode 100644 index 0000000000..0d7115fccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.scss @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$mainColor: var(--tb-slider-main-color, #5469FF); +$hoverRippleColor: var(--tb-slider-hover-ripple-color, rgba(84, 105, 255, 0.05)); +$focusRippleColor: var(--tb-slider-focus-ripple-color, rgba(84, 105, 255, 0.2)); +$backgroundColor: var(--tb-slider-background-color, #CCD2FF); +$tickMarksColor: var(--tb-slider-tick-marks-color, #5469FF); + +$mainColorDisabled: var(--tb-slider-main-color-disabled, #9BA2B0); +$backgroundColorDisabled: var(--tb-slider-background-color-disabled, #D5D7E5); + +.tb-slider-panel { + + .mat-mdc-slider.mat-primary.tb-slider { + --mdc-slider-active-track-color: #{$mainColor}; + --mdc-slider-handle-color: #{$mainColor}; + --mdc-slider-focus-handle-color: #{$mainColor}; + --mdc-slider-hover-handle-color: #{$mainColor}; + + --mdc-slider-with-tick-marks-inactive-container-color: #{$tickMarksColor}; + --mat-mdc-slider-ripple-color: #{$mainColor}; + + --mat-mdc-slider-hover-ripple-color: #{$hoverRippleColor}; + --mat-mdc-slider-focus-ripple-color: #{$focusRippleColor}; + + --mdc-slider-inactive-track-color: #{$backgroundColor}; + + --mdc-slider-disabled-active-track-color: #{$mainColorDisabled}; + --mdc-slider-disabled-handle-color: #{$mainColorDisabled}; + + --mdc-slider-disabled-inactive-track-color: #{$backgroundColorDisabled}; + --mdc-slider-with-tick-marks-disabled-container-color: #{$mainColorDisabled}; + + --mdc-slider-handle-width: 16px; + --mdc-slider-handle-height: 16px; + } + + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + padding: 20px 24px 24px 24px; + gap: 8px; + > div:not(.tb-slider-overlay), > tb-icon { + z-index: 1; + } + .tb-slider-overlay { + position: absolute; + inset: 12px; + } + div.tb-slider-title-panel { + z-index: 2; + } + .tb-slider-content { + flex: 1; + min-height: 0; + display: flex; + position: relative; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + .tb-slider-value-container { + min-height: 0; + } + .tb-slider-value { + white-space: nowrap; + } + .tb-slider-container { + align-self: stretch; + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 8px; + &.tb-min-height { + height: 6px; + } + .tb-slider-column { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + .mat-mdc-slider.tb-slider { + margin: 0; + height: 6px; + min-height: 6px; + min-width: 0; + &.mdc-slider--disabled { + opacity: 1; + } + .mdc-slider__track--inactive { + opacity: 1; + } + .mdc-slider__tick-marks { + .mdc-slider__tick-mark--active { + display: none; + } + .mdc-slider__tick-mark--inactive { + opacity: 1; + } + } + .mdc-slider__thumb.mat-mdc-slider-visual-thumb { + top: -21px; + .mat-ripple { + overflow: visible; + } + } + .mdc-slider__value-indicator-text { + white-space: nowrap; + } + } + .tb-slider-ticks { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts new file mode 100644 index 0000000000..37b5319d44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.component.ts @@ -0,0 +1,347 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; +import { + backgroundStyle, + ComponentStyle, + iconStyle, + overlayStyle, + textStyle +} from '@shared/models/widget-settings.models'; +import { Observable } from 'rxjs'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ValueType } from '@shared/models/constants'; +import { UtilsService } from '@core/services/utils.service'; +import { + SliderLayout, + sliderWidgetDefaultSettings, + SliderWidgetSettings +} from '@home/components/widget/lib/rpc/slider-widget.models'; +import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import tinycolor from 'tinycolor2'; + +@Component({ + selector: 'tb-slider-widget', + templateUrl: './slider-widget.component.html', + styleUrls: ['../action/action-widget.scss', './slider-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SliderWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('sliderContent', {static: false}) + sliderContent: ElementRef; + + @ViewChild('sliderValueContainer', {static: false}) + sliderValueContainer: ElementRef; + + @ViewChild('sliderValue', {static: false}) + sliderValue: ElementRef; + + @ViewChild('sliderTickMinContainer', {static: false}) + sliderTickMinContainer: ElementRef; + + @ViewChild('sliderTickMin', {static: false}) + sliderTickMin: ElementRef; + + @ViewChild('sliderTickMaxContainer', {static: false}) + sliderTickMaxContainer: ElementRef; + + @ViewChild('sliderTickMax', {static: false}) + sliderTickMax: ElementRef; + + @ViewChild('leftSliderIconContainer', {static: false, read: ElementRef}) + leftSliderIconContainer: ElementRef; + + @ViewChild('leftSliderIcon', {static: false, read: ElementRef}) + leftSliderIcon: ElementRef; + + @ViewChild('rightSliderIconContainer', {static: false, read: ElementRef}) + rightSliderIconContainer: ElementRef; + + @ViewChild('rightSliderIcon', {static: false, read: ElementRef}) + rightSliderIcon: ElementRef; + + settings: SliderWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + value: number = null; + private prevValue: number = null; + + disabled = false; + + layout: SliderLayout; + + showValue = true; + valueText = 'N/A'; + valueStyle: ComponentStyle = {}; + + showLeftRightIcon = false; + leftIcon = ''; + leftIconStyle: ComponentStyle = {}; + rightIcon = ''; + rightIconStyle: ComponentStyle = {}; + + showTicks = true; + ticksStyle: ComponentStyle = {}; + + sliderStep: number = undefined; + + autoScale = false; + + showWidgetTitlePanel = this.widgetComponent.dashboardWidget.showWidgetTitlePanel; + + sliderValueText = this._sliderValueText.bind(this); + + private panelResize$: ResizeObserver; + + private valueSetter: ValueSetter; + + private sliderCssClass: string; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + private utils: UtilsService, + private widgetComponent: WidgetComponent, + protected cd: ChangeDetectorRef, + private elementRef: ElementRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...sliderWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + this.layout = this.settings.layout; + + this.autoScale = this.settings.autoScale; + + this.showValue = this.layout !== SliderLayout.simplified && this.settings.showValue; + this.valueStyle = textStyle(this.settings.valueFont); + this.valueStyle.color = this.settings.valueColor; + + this.showLeftRightIcon = this.layout === SliderLayout.extended; + if (this.showLeftRightIcon) { + this.leftIcon = this.settings.leftIcon; + this.leftIconStyle = iconStyle(this.settings.leftIconSize, this.settings.leftIconSizeUnit ); + this.rightIcon = this.settings.rightIcon; + this.rightIconStyle = iconStyle(this.settings.rightIconSize, this.settings.rightIconSizeUnit ); + if (!this.autoScale) { + const leftIconMargin = this.settings.leftIconSize / 2 + (this.settings.leftIconSizeUnit || 'px'); + this.leftIconStyle.marginTop = `calc(-${leftIconMargin} + 3px)`; + const rightIconMargin = this.settings.rightIconSize / 2 + (this.settings.rightIconSizeUnit || 'px'); + this.rightIconStyle.marginTop = `calc(-${rightIconMargin} + 3px)`; + } + } + + this.showTicks = this.settings.showTicks; + if (this.showTicks) { + this.ticksStyle = textStyle(this.settings.ticksFont); + this.ticksStyle.color = this.settings.ticksColor; + } + + if (this.settings.showTickMarks) { + const range = this.settings.tickMax - this.settings.tickMin; + this.sliderStep = range / (this.settings.tickMarksCount - 1); + } + + const mainColorInstance = tinycolor(this.settings.mainColor); + const hoverRippleColor = mainColorInstance.clone().setAlpha(mainColorInstance.getAlpha() * 0.05).toRgbString(); + const focusRippleColor = mainColorInstance.clone().setAlpha(mainColorInstance.getAlpha() * 0.2).toRgbString(); + + const sliderVariablesCss = `.tb-slider-panel {\n`+ + `--tb-slider-main-color: ${this.settings.mainColor};\n`+ + `--tb-slider-background-color: ${this.settings.backgroundColor};\n`+ + `--tb-slider-hover-ripple-color: ${hoverRippleColor};\n`+ + `--tb-slider-focus-ripple-color: ${focusRippleColor};\n`+ + `--tb-slider-tick-marks-color: ${this.settings.tickMarksColor};\n`+ + `--tb-slider-main-color-disabled: ${this.settings.mainColorDisabled};\n`+ + `--tb-slider-background-disabled: ${this.settings.backgroundColorDisabled};\n`+ + `}`; + this.sliderCssClass = + this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-slider', sliderVariablesCss); + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.slider.initial-value')}; + this.createValueGetter(getInitialStateSettings, ValueType.INTEGER, { + next: (value) => this.onValue(value) + }); + + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onDisabled(value) + }); + + const valueChangeSettings = {...this.settings.valueChange, + actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')}; + this.valueSetter = this.createValueSetter(valueChangeSettings); + } + + ngAfterViewInit(): void { + if (this.autoScale) { + this.panelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.panelResize$.observe(this.sliderContent.nativeElement); + if (this.showValue) { + this.panelResize$.observe(this.sliderValueContainer.nativeElement); + } + this.onResize(); + } + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + if (this.sliderCssClass) { + this.utils.clearCssElement(this.renderer, this.sliderCssClass); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onSliderChange() { + this.updateValueText(); + if (!this.ctx.isEdit && !this.ctx.isPreview) { + const prevValue = this.prevValue; + const targetValue = this.value; + this.updateValue(this.valueSetter, targetValue, { + next: () => this.onValue(targetValue), + error: () => this.onValue(prevValue) + }); + } + } + + private _sliderValueText(value: number): string { + return formatValue(value, this.settings.valueDecimals, this.settings.valueUnits, false); + } + + private onValue(value: number): void { + this.value = value; + this.prevValue = value; + this.updateValueText(); + this.cd.markForCheck(); + } + + private updateValueText() { + if (isDefinedAndNotNull(this.value) && isNumeric(this.value)) { + this.valueText = formatValue(this.value, this.settings.valueDecimals, this.settings.valueUnits, false); + } else { + this.valueText = 'N/A'; + } + } + + private onDisabled(value: boolean): void { + this.disabled = !!value; + this.cd.markForCheck(); + } + + private onResize() { + const panelWidth = this.sliderContent.nativeElement.getBoundingClientRect().width; + const panelHeight = this.sliderContent.nativeElement.getBoundingClientRect().height; + + if (this.showValue) { + this.resetScale(this.sliderValueContainer.nativeElement, this.sliderValue.nativeElement); + } + + if (this.showLeftRightIcon) { + this.resetScale(this.leftSliderIconContainer.nativeElement, this.leftSliderIcon.nativeElement); + this.resetScale(this.rightSliderIconContainer.nativeElement, this.rightSliderIcon.nativeElement); + } + + if (this.showTicks) { + this.resetScale(this.sliderTickMinContainer.nativeElement, this.sliderTickMin.nativeElement); + this.resetScale(this.sliderTickMaxContainer.nativeElement, this.sliderTickMax.nativeElement); + } + + let minAspect = 0.2; + let avgContentHeight = 35; + if (this.showTicks) { + minAspect += 0.1; + avgContentHeight += 20; + } + if (this.showValue) { + minAspect += 0.1; + avgContentHeight += 50; + } + const aspect = Math.min(panelHeight / panelWidth, minAspect); + const targetHeight = panelWidth * aspect; + const scale = targetHeight / avgContentHeight; + + if (this.showValue) { + this.updateScale(this.sliderValueContainer.nativeElement, this.sliderValue.nativeElement, scale); + } + if (this.showLeftRightIcon) { + const leftIconContainerRect = this.leftSliderIconContainer.nativeElement.getBoundingClientRect(); + const leftIconContainerMarginTop = -(leftIconContainerRect.width * scale) / 2 + 3; + this.renderer.setStyle(this.leftSliderIconContainer.nativeElement, 'marginTop', `${leftIconContainerMarginTop}px`); + this.updateScale(this.leftSliderIconContainer.nativeElement, this.leftSliderIcon.nativeElement, scale, true); + const rightIconContainerRect = this.rightSliderIconContainer.nativeElement.getBoundingClientRect(); + const rightIconContainerMarginTop = -(rightIconContainerRect.width * scale) / 2 + 3; + this.renderer.setStyle(this.rightSliderIconContainer.nativeElement, 'marginTop', `${rightIconContainerMarginTop}px`); + this.updateScale(this.rightSliderIconContainer.nativeElement, this.rightSliderIcon.nativeElement, scale, true); + } + if (this.showTicks) { + this.updateScale(this.sliderTickMinContainer.nativeElement, this.sliderTickMin.nativeElement, scale); + this.updateScale(this.sliderTickMaxContainer.nativeElement, this.sliderTickMax.nativeElement, scale); + } + } + + private resetScale(container: HTMLElement, element: HTMLElement): void { + this.renderer.setStyle(container, 'width', ''); + this.renderer.setStyle(container, 'height', ''); + this.renderer.setStyle(element, 'transform', ''); + } + + private updateScale(container: HTMLElement, element: HTMLElement, scale: number, sameHeight = false): void { + const rect = container.getBoundingClientRect(); + this.renderer.setStyle(container, 'width', `${rect.width * scale}px`); + this.renderer.setStyle(container, 'height', `${(sameHeight ? rect.width : rect.height) * scale}px`); + this.renderer.setStyle(element, 'transform', `scale(${scale})`); + this.renderer.setStyle(element, 'transform-origin', 'left top'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.models.ts new file mode 100644 index 0000000000..c6fe1e290a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/slider-widget.models.ts @@ -0,0 +1,196 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + DataToValueType, + GetValueAction, + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; + +export enum SliderLayout { + default = 'default', + extended = 'extended', + simplified = 'simplified' +} + +export const sliderLayouts = Object.keys(SliderLayout) as SliderLayout[]; + +export const sliderLayoutTranslations = new Map( + [ + [SliderLayout.default, 'widgets.slider.layout-default'], + [SliderLayout.extended, 'widgets.slider.layout-extended'], + [SliderLayout.simplified, 'widgets.slider.layout-simplified'] + ] +); + +export const sliderLayoutImages = new Map( + [ + [SliderLayout.default, 'assets/widget/slider/default-layout.svg'], + [SliderLayout.extended, 'assets/widget/slider/extended-layout.svg'], + [SliderLayout.simplified, 'assets/widget/slider/simplified-layout.svg'] + ] +); + +export interface SliderWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + valueChange: SetValueSettings; + layout: SliderLayout; + autoScale: boolean; + showValue: boolean; + valueUnits: string; + valueDecimals: number; + valueFont: Font; + valueColor: string; + showTicks: boolean; + tickMin: number; + tickMax: number; + ticksFont: Font; + ticksColor: string; + showTickMarks: boolean; + tickMarksCount: number; + tickMarksColor: string; + mainColor: string; + backgroundColor: string; + mainColorDisabled: string; + backgroundColorDisabled: string; + leftIcon: string; + leftIconSize: number; + leftIconSizeUnit: cssUnit; + leftIconColor: string; + rightIcon: string; + rightIconSize: number; + rightIconSizeUnit: cssUnit; + rightIconColor: string; + background: BackgroundSettings; +} + +export const sliderWidgetDefaultSettings: SliderWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: 0, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return integer value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + valueChange: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SHARED_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.VALUE, + constantValue: 0, + valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;' + } + }, + layout: SliderLayout.default, + autoScale: true, + showValue: true, + valueUnits: '%', + valueDecimals: 0, + valueFont: { + family: 'Roboto', + size: 36, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '36px' + }, + valueColor: 'rgba(0, 0, 0, 0.87)', + showTicks: true, + tickMin: 0, + tickMax: 100, + ticksFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + ticksColor: 'rgba(0,0,0,0.54)', + showTickMarks: true, + tickMarksCount: 11, + tickMarksColor: '#5469FF', + mainColor: '#5469FF', + backgroundColor: '#CCD2FF', + mainColorDisabled: '#9BA2B0', + backgroundColorDisabled: '#D5D7E5', + leftIcon: 'lightbulb', + leftIconSize: 24, + leftIconSizeUnit: 'px', + leftIconColor: '#5469FF', + rightIcon: 'mdi:lightbulb-on', + rightIconSize: 24, + rightIconSizeUnit: 'px', + rightIconColor: '#5469FF', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html new file mode 100644 index 0000000000..b37030c884 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.html @@ -0,0 +1,142 @@ + + +
+
widgets.power-button.behavior
+
+
widgets.rpc-state.initial-state
+ +
+
+
widgets.power-button.power-on
+ +
+
+
widgets.power-button.power-off
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.card-style
+ + + {{ powerButtonLayoutTranslationMap.get(layout) | translate }} + + +
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
+
widgets.power-button.button
+
+
{{ 'widgets.power-button.power-on-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.power-off-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.disabled-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts new file mode 100644 index 0000000000..c1d9f98460 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/power-button-widget-settings.component.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ValueType } from '@shared/models/constants'; +import { + powerButtonDefaultSettings, + powerButtonLayoutImages, + powerButtonLayouts, + powerButtonLayoutTranslations +} from '@home/components/widget/lib/rpc/power-button-widget.models'; + +@Component({ + selector: 'tb-power-button-widget-settings', + templateUrl: './power-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class PowerButtonWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + return this.widgetConfig?.config?.targetDevice; + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + + powerButtonLayouts = powerButtonLayouts; + + powerButtonLayoutTranslationMap = powerButtonLayoutTranslations; + powerButtonLayoutImageMap = powerButtonLayoutImages; + + valueType = ValueType; + + powerButtonWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.powerButtonWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...powerButtonDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.powerButtonWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + onUpdateState: [settings.onUpdateState, []], + offUpdateState: [settings.offUpdateState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + mainColorOn: [settings.mainColorOn, []], + backgroundColorOn: [settings.backgroundColorOn, []], + + mainColorOff: [settings.mainColorOff, []], + backgroundColorOff: [settings.backgroundColorOff, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + background: [settings.background, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html index de641b8808..31c8276666 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html @@ -35,7 +35,7 @@ {{ 'widgets.progress-bar.auto-scale' | translate }}
-
+
{{ 'widgets.progress-bar.value' | translate }} @@ -48,7 +48,7 @@
-
+
{{ 'widgets.progress-bar.range' | translate }}
widgets.progress-bar.min
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html index b1fccafac0..4cf5857612 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html @@ -97,7 +97,8 @@
{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ? 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}
- {{ 'widgets.value-action.converter-constant' | translate }} + {{ 'widgets.value-action.converter-value' | translate }} + {{ 'widgets.value-action.converter-constant' | translate }} {{ 'widgets.value-action.converter-function' | translate }} {{ 'widgets.value-action.converter-none' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts index d718168b11..819ed2a3fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts @@ -32,6 +32,7 @@ import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; import { IAliasController } from '@core/api/widget-api.models'; import { WidgetService } from '@core/http/widget.service'; +import { ValueType } from '@shared/models/constants'; @Component({ selector: 'tb-set-value-action-settings-panel', @@ -45,6 +46,9 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen @Input() panelTitle: string; + @Input() + valueType = ValueType.BOOLEAN; + @Input() setValueSettings: SetValueSettings; @@ -79,6 +83,8 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + ValueType = ValueType; + setValueSettingsFormGroup: UntypedFormGroup; constructor(private fb: UntypedFormBuilder, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts index fce1e23673..7b4c8e0428 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts @@ -36,6 +36,7 @@ import { isDefinedAndNotNull } from '@core/utils'; import { SetValueActionSettingsPanelComponent } from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component'; +import { ValueType } from '@shared/models/constants'; @Component({ selector: 'tb-set-value-action-settings', @@ -58,6 +59,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce @Input() panelTitle: string; + @Input() + valueType = ValueType.BOOLEAN; + @Input() aliasController: IAliasController; @@ -114,6 +118,7 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce const ctx: any = { setValueSettings: this.modelValue, panelTitle: this.panelTitle, + valueType: this.valueType, aliasController: this.aliasController, targetDevice: this.targetDevice, widgetType: this.widgetType @@ -137,6 +142,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce private updateDisplayValue() { let value: any; switch (this.modelValue.valueToData.type) { + case ValueToDataType.VALUE: + value = 'value'; + break; case ValueToDataType.CONSTANT: value = this.modelValue.valueToData.constantValue; break; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index 35ca3f3144..3752fb3b3c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -80,6 +80,22 @@
+ +
+
{{ 'widget-action.URL' | translate }}
+ + + + warning + + +
+
@@ -89,7 +105,8 @@
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index c93a05fafd..6c2cd3fb8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts @@ -279,6 +279,16 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali this.fb.control(action ? action.mobileAction : null, [Validators.required]) ); break; + case WidgetActionType.openURL: + this.actionTypeFormGroup.addControl( + 'openNewBrowserTab', + this.fb.control(action ? action.openNewBrowserTab : false, []) + ); + this.actionTypeFormGroup.addControl( + 'url', + this.fb.control(action ? action.url : null, [Validators.required]) + ); + break; } } this.actionTypeFormGroupSubscriptions.push( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html index b4ce9c8ce2..bdecc9d36b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html @@ -56,7 +56,7 @@
-
+
{{ 'widgets.button.color-palette' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 9350c2d932..9208efd68a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -81,7 +81,7 @@ {{ 'widgets.single-switch.auto-scale' | translate }}
-
+
{{ 'widgets.single-switch.label' | translate }} @@ -98,7 +98,7 @@
-
+
{{ 'widgets.single-switch.icon' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html new file mode 100644 index 0000000000..0f3c3f11a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.html @@ -0,0 +1,216 @@ + + +
+
widgets.slider.behavior
+
+
widgets.slider.initial-value
+ +
+
+
widgets.slider.on-value-change
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.card-style
+ + + {{ sliderLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.slider.auto-scale' | translate }} + +
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
+
widgets.slider.slider
+
+ + {{ 'widgets.slider.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+
+
{{ 'widgets.slider.range' | translate }}
+
+
widgets.slider.min
+ + + +
widgets.slider.max
+ + + +
+
+
+ + {{ 'widgets.slider.range-ticks' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.slider.tick-marks' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.slider.colors' | translate }}
+
+
+
widgets.slider.main
+ + +
+ +
+
widgets.slider.background
+ + +
+
+
+
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
+
+
widgets.slider.main
+ + +
+ +
+
widgets.slider.background
+ + +
+
+
+
+
+ {{ 'widgets.slider.left-icon' | translate }} +
+
+ + + + + + + + +
+
+
+
+ {{ 'widgets.slider.right-icon' | translate }} +
+
+ + + + + + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts new file mode 100644 index 0000000000..a215a93d88 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slider-widget-settings.component.ts @@ -0,0 +1,186 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ValueType } from '@shared/models/constants'; +import { + SliderLayout, + sliderLayoutImages, + sliderLayouts, + sliderLayoutTranslations, + sliderWidgetDefaultSettings +} from '@home/components/widget/lib/rpc/slider-widget.models'; +import { formatValue } from '@core/utils'; + +@Component({ + selector: 'tb-slider-widget-settings', + templateUrl: './slider-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class SliderWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + return this.widgetConfig?.config?.targetDevice; + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + + sliderLayout = SliderLayout; + + sliderLayouts = sliderLayouts; + + sliderLayoutTranslationMap = sliderLayoutTranslations; + sliderLayoutImageMap = sliderLayoutImages; + + valueType = ValueType; + + sliderWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.sliderWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...sliderWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.sliderWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + valueChange: [settings.valueChange, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + autoScale: [settings.autoScale, []], + + showValue: [settings.showValue, []], + valueUnits: [settings.valueUnits, []], + valueDecimals: [settings.valueDecimals, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + tickMin: [settings.tickMin, []], + tickMax: [settings.tickMax, []], + + showTicks: [settings.showTicks, []], + ticksFont: [settings.ticksFont, []], + ticksColor: [settings.ticksColor, []], + + showTickMarks: [settings.showTickMarks, []], + tickMarksCount: [settings.tickMarksCount, [Validators.min(2)]], + tickMarksColor: [settings.tickMarksColor, []], + + mainColor: [settings.mainColor, []], + backgroundColor: [settings.backgroundColor, []], + + mainColorDisabled: [settings.mainColorDisabled, []], + backgroundColorDisabled: [settings.backgroundColorDisabled, []], + + leftIconSize: [settings.leftIconSize, [Validators.min(0)]], + leftIconSizeUnit: [settings.leftIconSizeUnit, []], + leftIcon: [settings.leftIcon, []], + leftIconColor: [settings.leftIconColor, []], + + rightIconSize: [settings.rightIconSize, [Validators.min(0)]], + rightIconSizeUnit: [settings.rightIconSizeUnit, []], + rightIcon: [settings.rightIcon, []], + rightIconColor: [settings.rightIconColor, []], + + background: [settings.background, []], + }); + } + + protected validatorTriggers(): string[] { + return ['showValue', 'showTicks', 'showTickMarks', 'layout']; + } + + protected updateValidators(_emitEvent: boolean): void { + const showValue: boolean = this.sliderWidgetSettingsForm.get('showValue').value; + const showTicks: boolean = this.sliderWidgetSettingsForm.get('showTicks').value; + const showTickMarks: boolean = this.sliderWidgetSettingsForm.get('showTickMarks').value; + const layout: SliderLayout = this.sliderWidgetSettingsForm.get('layout').value; + + const valueEnabled = layout !== SliderLayout.simplified; + const leftRightIconsEnabled = layout === SliderLayout.extended; + + if (valueEnabled && showValue) { + this.sliderWidgetSettingsForm.get('valueUnits').enable(); + this.sliderWidgetSettingsForm.get('valueDecimals').enable(); + this.sliderWidgetSettingsForm.get('valueFont').enable(); + this.sliderWidgetSettingsForm.get('valueColor').enable(); + } else { + this.sliderWidgetSettingsForm.get('valueUnits').disable(); + this.sliderWidgetSettingsForm.get('valueDecimals').disable(); + this.sliderWidgetSettingsForm.get('valueFont').disable(); + this.sliderWidgetSettingsForm.get('valueColor').disable(); + } + + if (showTicks) { + this.sliderWidgetSettingsForm.get('ticksFont').enable(); + this.sliderWidgetSettingsForm.get('ticksColor').enable(); + } else { + this.sliderWidgetSettingsForm.get('ticksFont').disable(); + this.sliderWidgetSettingsForm.get('ticksColor').disable(); + } + + if (showTickMarks) { + this.sliderWidgetSettingsForm.get('tickMarksCount').enable(); + this.sliderWidgetSettingsForm.get('tickMarksColor').enable(); + } else { + this.sliderWidgetSettingsForm.get('tickMarksCount').disable(); + this.sliderWidgetSettingsForm.get('tickMarksColor').disable(); + } + + if (leftRightIconsEnabled) { + this.sliderWidgetSettingsForm.get('leftIconSize').enable(); + this.sliderWidgetSettingsForm.get('leftIconSizeUnit').enable(); + this.sliderWidgetSettingsForm.get('leftIcon').enable(); + this.sliderWidgetSettingsForm.get('leftIconColor').enable(); + this.sliderWidgetSettingsForm.get('rightIconSize').enable(); + this.sliderWidgetSettingsForm.get('rightIconSizeUnit').enable(); + this.sliderWidgetSettingsForm.get('rightIcon').enable(); + this.sliderWidgetSettingsForm.get('rightIconColor').enable(); + } else { + this.sliderWidgetSettingsForm.get('leftIconSize').disable(); + this.sliderWidgetSettingsForm.get('leftIconSizeUnit').disable(); + this.sliderWidgetSettingsForm.get('leftIcon').disable(); + this.sliderWidgetSettingsForm.get('leftIconColor').disable(); + this.sliderWidgetSettingsForm.get('rightIconSize').disable(); + this.sliderWidgetSettingsForm.get('rightIconSizeUnit').disable(); + this.sliderWidgetSettingsForm.get('rightIcon').disable(); + this.sliderWidgetSettingsForm.get('rightIconColor').disable(); + } + } + + private _valuePreviewFn(): string { + const units: string = this.sliderWidgetSettingsForm.get('valueUnits').value; + const decimals: number = this.sliderWidgetSettingsForm.get('valueDecimals').value; + return formatValue(48, decimals, units, false); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html index e742475e80..0040e7fa64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html @@ -19,8 +19,8 @@
-
-
widgets.liquid-level-card.shape
+
+
widgets.liquid-level-card.shape
{{ DataSourceTypeTranslations.get(type) | translate }} @@ -51,7 +51,7 @@ formControlName="shapeAttributeName">
-
+
widgets.liquid-level-card.shape-by-attribute
-
+
widgets.liquid-level-card.widget-units
-
+
@@ -111,9 +111,9 @@
-
+
widgets.liquid-level-card.total-volume
-
+
@@ -141,10 +141,30 @@ formControlName="volumeAttributeName"> - +
+
+
widgets.liquid-level-card.total-volume-units
+
+ + + + {{ DataSourceTypeTranslations.get(type) | translate }} + + + + + + + +
@@ -168,7 +188,7 @@
widgets.liquid-level-card.value
-
+
@@ -178,7 +198,7 @@
widgets.liquid-level-card.total-volume
-
+
@@ -212,7 +232,7 @@ {{ 'widgets.liquid-level-card.level' | translate }} -
+
@@ -232,7 +252,7 @@ {{ 'widgets.value-card.date' | translate }} -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts index 5ed3bea446..e25e02d1b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.ts @@ -158,7 +158,9 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon volumeSource: [settings.volumeSource, []], volumeConstant: [settings.volumeConstant, [Validators.required, Validators.min(0.1)]], volumeAttributeName: [settings.volumeAttributeName, [Validators.required]], + volumeUnitsSource: [settings.volumeUnitsSource, []], volumeUnits: [settings.volumeUnits, [Validators.required]], + volumeUnitsAttributeName: [settings.volumeUnitsAttributeName, [Validators.required]], volumeFont: [settings.volumeFont, []], volumeColor: [settings.volumeColor, []], valueFont: [settings.valueFont, []], @@ -195,7 +197,7 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon protected validatorTriggers(): string[] { return [ 'showBackgroundOverlay', 'showTooltip', 'showTooltipLevel', 'tankSelectionType', 'datasourceUnits', - 'showTooltipDate', 'layout', 'volumeSource', 'widgetUnitsSource' + 'showTooltipDate', 'layout', 'volumeSource', 'widgetUnitsSource', 'volumeUnitsSource' ]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 3f4a619c49..8fc6ef96c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -321,6 +321,12 @@ import { import { CommandButtonWidgetSettingsComponent } from '@home/components/widget/lib/settings/button/command-button-widget-settings.component'; +import { + PowerButtonWidgetSettingsComponent +} from '@home/components/widget/lib/settings/button/power-button-widget-settings.component'; +import { + SliderWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/slider-widget-settings.component'; @NgModule({ declarations: [ @@ -436,7 +442,9 @@ import { BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, - CommandButtonWidgetSettingsComponent + CommandButtonWidgetSettingsComponent, + PowerButtonWidgetSettingsComponent, + SliderWidgetSettingsComponent ], imports: [ CommonModule, @@ -557,7 +565,9 @@ import { BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, - CommandButtonWidgetSettingsComponent + CommandButtonWidgetSettingsComponent, + PowerButtonWidgetSettingsComponent, + SliderWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -645,5 +655,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type 0) { diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 2217b5bf5d..d6c07004c0 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -15,7 +15,7 @@ /// import { AfterViewInit, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { startWith, skip, Subject } from 'rxjs'; +import { skip, startWith, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; @@ -32,6 +32,8 @@ import { instanceOfSearchableComponent, ISearchableComponent } from '@home/model import { ActiveComponentService } from '@core/services/active-component.service'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-home', @@ -70,8 +72,8 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni constructor(protected store: Store, @Inject(WINDOW) private window: Window, private activeComponentService: ActiveComponentService, - public breakpointObserver: BreakpointObserver, - private fb: FormBuilder) { + private fb: FormBuilder, + public breakpointObserver: BreakpointObserver) { super(store); } @@ -144,9 +146,18 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni private updateActiveComponent(activeComponent: any) { this.showSearch = false; + this.hideLoadingBar = false; this.textSearch.reset('', {emitEvent: false}); this.activeComponent = activeComponent; - this.hideLoadingBar = activeComponent && activeComponent instanceof RouterTabsComponent; + + if (activeComponent && activeComponent instanceof RouterTabsComponent + && isDefinedAndNotNull(this.activeComponent.activatedRoute?.snapshot?.data?.showMainLoadingBar)) { + this.hideLoadingBar = !this.activeComponent.activatedRoute.snapshot.data.showMainLoadingBar; + } else if (activeComponent && activeComponent instanceof PageComponent + && isDefinedAndNotNull(this.activeComponent?.showMainLoadingBar)) { + this.hideLoadingBar = !this.activeComponent.showMainLoadingBar; + } + if (this.activeComponent && instanceOfSearchableComponent(this.activeComponent)) { this.searchEnabled = true; this.searchableComponent = this.activeComponent; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index a834120dc6..2ea34377d6 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -57,6 +57,7 @@ import { createLabelFromDatasource, createLabelFromSubscriptionEntityInfo, formatValue, + getEntityDetailsPageURL, hasDatasourceLabelsVariables, isDefined } from '@core/utils'; @@ -253,7 +254,8 @@ export class WidgetContext { }; utils: IWidgetUtils = { - formatValue + formatValue, + getEntityDetailsPageURL }; $widgetElement: JQuery; @@ -347,35 +349,35 @@ export class WidgetContext { showSuccessToast(message: string, duration: number = 1000, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', - target: string = 'dashboardRoot') { - this.showToast('success', message, duration, verticalPosition, horizontalPosition, target); + target: string = 'dashboardRoot', modern = false) { + this.showToast('success', message, duration, verticalPosition, horizontalPosition, target, modern); } showInfoToast(message: string, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', - target: string = 'dashboardRoot') { - this.showToast('info', message, undefined, verticalPosition, horizontalPosition, target); + target: string = 'dashboardRoot', modern = false) { + this.showToast('info', message, undefined, verticalPosition, horizontalPosition, target, modern); } showWarnToast(message: string, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', - target: string = 'dashboardRoot') { - this.showToast('warn', message, undefined, verticalPosition, horizontalPosition, target); + target: string = 'dashboardRoot', modern = false) { + this.showToast('warn', message, undefined, verticalPosition, horizontalPosition, target, modern); } showErrorToast(message: string, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', - target: string = 'dashboardRoot') { - this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target); + target: string = 'dashboardRoot', modern = false) { + this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target, modern); } showToast(type: NotificationType, message: string, duration: number, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', - target: string = 'dashboardRoot') { + target: string = 'dashboardRoot', modern = false) { this.store.dispatch(new ActionNotificationShow( { message, @@ -385,7 +387,8 @@ export class WidgetContext { horizontalPosition, target, panelClass: this.widgetNamespace, - forceDismiss: true + forceDismiss: true, + modern })); } diff --git a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts index 252250ece2..20e6cc73e1 100644 --- a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts @@ -33,6 +33,7 @@ const routes: Routes = [ component: RouterTabsComponent, data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + showMainLoadingBar: false, breadcrumb: { label: 'account.account', icon: 'account_circle' diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 74c5012791..63fb9a14de 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -136,6 +136,7 @@ const routes: Routes = [ component: RouterTabsComponent, data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + showMainLoadingBar: false, breadcrumb: { label: 'admin.settings', icon: 'settings' diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index 4b0ba741d3..1d346c70f1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -15,7 +15,14 @@ /// import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { + AbstractControl, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + Validators +} from '@angular/forms'; import { ClientAuthenticationMethod, DomainSchema, @@ -26,9 +33,11 @@ import { MapperConfigType, OAuth2ClientRegistrationTemplate, OAuth2DomainInfo, - OAuth2Info, OAuth2MobileInfo, + OAuth2Info, + OAuth2MobileInfo, OAuth2ParamsInfo, - OAuth2RegistrationInfo, PlatformType, + OAuth2RegistrationInfo, + PlatformType, platformTypeTranslations, TenantNameStrategy } from '@shared/models/oauth2.models'; @@ -105,6 +114,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha templateProvider = ['Custom']; + showMainLoadingBar = false; + private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; private static validateScope(control: AbstractControl): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index e4fd483eb9..17dda4a0c9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -23,7 +23,8 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormControl, - UntypedFormGroup, ValidationErrors, + UntypedFormGroup, + ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; @@ -35,10 +36,7 @@ import { randomAlphanumeric } from '@core/utils'; import { AuthService } from '@core/auth/auth.service'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; -import { forkJoin, Observable, of } from 'rxjs'; -import { MatCheckboxChange } from '@angular/material/checkbox'; -import { AlarmInfo } from '@shared/models/alarm.models'; -import { QueueProcessingStrategyTypes, QueueProcessingStrategyTypesMap } from '@shared/models/queue.models'; +import { Observable, of } from 'rxjs'; @Component({ selector: 'tb-security-settings', @@ -50,6 +48,8 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi securitySettingsFormGroup: UntypedFormGroup; jwtSecuritySettingsFormGroup: UntypedFormGroup; + showMainLoadingBar = false; + private securitySettings: SecuritySettings; private jwtSettings: JwtSettings; diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 5ea4776ff9..5355114586 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -47,6 +47,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI twoFactorAuthProviderType = TwoFactorAuthProviderType; twoFactorAuthProvidersData = twoFactorAuthProvidersData; + showMainLoadingBar = false; + @ViewChildren(MatExpansionPanel) expansionPanel: QueryList; constructor(protected store: Store, diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.models.ts b/ui-ngx/src/app/modules/home/pages/home-pages.models.ts index 20872e3c65..848bb0b85c 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.models.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.models.ts @@ -30,4 +30,3 @@ export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction + + {{ 'notification.edge-trigger-settings' | translate }} + +
+
+ notification.filter + + + + notification.notify-on + + + {{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }} + + + +
+
+ +
+
+ + notification.description + + +
+
+
+ + + {{ 'notification.edge-trigger-settings' | translate }} +
+
+
+ notification.filter + + +
+
+
+
+
+ + notification.description + + +
+
+
+ {{ 'notification.entities-limit-trigger-settings' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts index 9604670757..34ff605027 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts @@ -68,6 +68,7 @@ import { } from '@shared/models/api-usage.models'; import { LimitedApi, LimitedApiTranslationMap } from '@shared/models/limited-api.models'; import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { EdgeConnectionEvent, EdgeConnectionEventTranslationMap } from '@shared/models/edge.models'; export interface RuleNotificationDialogData { rule?: NotificationRule; @@ -98,6 +99,8 @@ export class RuleNotificationDialogComponent extends apiUsageLimitTemplateForm: FormGroup; newPlatformVersionTemplateForm: FormGroup; rateLimitsTemplateForm: FormGroup; + edgeCommunicationFailureTemplateForm: FormGroup; + edgeConnectionTemplateForm: FormGroup; triggerType = TriggerType; triggerTypes: TriggerType[]; @@ -132,6 +135,9 @@ export class RuleNotificationDialogComponent extends apiFeatures: ApiFeature[] = Object.values(ApiFeature); apiFeatureTranslationMap = ApiFeatureTranslationMap; + edgeConnectionEvents: EdgeConnectionEvent[] = Object.values(EdgeConnectionEvent); + edgeConnectionEventTranslationMap = EdgeConnectionEventTranslationMap; + limitedApis: StringItemsOption[]; entityType = EntityType; @@ -221,6 +227,19 @@ export class RuleNotificationDialogComponent extends } }); + this.edgeConnectionTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + edges: [null], + notifyOn: [null] + }) + }); + + this.edgeCommunicationFailureTemplateForm = this.fb.group({ + triggerConfig: this.fb.group({ + edges: [null] + }) + }); + this.alarmTemplateForm = this.fb.group({ triggerConfig: this.fb.group({ alarmTypes: [null], @@ -328,7 +347,9 @@ export class RuleNotificationDialogComponent extends [TriggerType.ENTITIES_LIMIT, this.entitiesLimitTemplateForm], [TriggerType.API_USAGE_LIMIT, this.apiUsageLimitTemplateForm], [TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm], - [TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm] + [TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm], + [TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm], + [TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm] ]); if (data.isAdd || data.isCopy) { diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html index 5d41da6b4a..ab3694773e 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -65,7 +65,7 @@ - > + diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.scss b/ui-ngx/src/app/shared/components/json-object-edit.component.scss index eb66f102a3..63884d518f 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.scss +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.scss @@ -52,6 +52,7 @@ width: 100%; min-width: 200px; height: 100%; + min-height: 300px; &:not(.fill-height) { min-height: 200px; diff --git a/ui-ngx/src/app/shared/components/page.component.ts b/ui-ngx/src/app/shared/components/page.component.ts index d06e5ccc7b..4a5ee06cbc 100644 --- a/ui-ngx/src/app/shared/components/page.component.ts +++ b/ui-ngx/src/app/shared/components/page.component.ts @@ -29,6 +29,8 @@ export abstract class PageComponent implements OnDestroy { loadingSubscription: Subscription; disabledOnLoadFormControls: Array = []; + showMainLoadingBar = true; + protected constructor(protected store: Store) { this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), share()); } diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html index cb39b98f48..ca70f6b476 100644 --- a/ui-ngx/src/app/shared/components/snack-bar-component.html +++ b/ui-ngx/src/app/shared/components/snack-bar-component.html @@ -15,15 +15,30 @@ limitations under the License. --> -
-
- + +
+ +
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.scss b/ui-ngx/src/app/shared/components/snack-bar-component.scss index 97531e220e..d86b740fc3 100644 --- a/ui-ngx/src/app/shared/components/snack-bar-component.scss +++ b/ui-ngx/src/app/shared/components/snack-bar-component.scss @@ -71,4 +71,42 @@ background: #008000; } } + + .tb-modern-toast { + display: flex; + flex-direction: column; + z-index: 1; + &-panel { + display: flex; + padding: 4px 4px 4px 12px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 4px; + box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2); + &.info-toast { + background: #e6e6e6; + color: rgba(50, 50, 50, 1); + } + &.warn-toast { + background: #fff3eb; + color: rgba(220, 109, 27, 1); + + } + &.error-toast { + background-color: #fff2f3; + color: rgba(209, 39, 48, 1); + } + &.success-toast { + background: #ebfcec; + color: rgba(0, 128, 0, 1); + } + } + .toast-text { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.scss b/ui-ngx/src/app/shared/components/time/timewindow.component.scss index 92673a41ad..99f3d8367d 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.scss @@ -58,6 +58,7 @@ .mdc-button__label { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } } } diff --git a/ui-ngx/src/app/shared/components/toast.directive.ts b/ui-ngx/src/app/shared/components/toast.directive.ts index 6881460f6a..582be5a577 100644 --- a/ui-ngx/src/app/shared/components/toast.directive.ts +++ b/ui-ngx/src/app/shared/components/toast.directive.ts @@ -84,6 +84,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy { if (hideNotification) { const target = hideNotification.target || 'root'; if (this.toastTarget === target) { + this.currentMessage = null; this.ngZone.run(() => { if (this.snackBarRef) { this.snackBarRef.dismiss(); @@ -280,7 +281,7 @@ export type ToastAnimationState = 'default' | 'opened' | 'closing'; }) export class TbSnackBarComponent implements AfterViewInit, OnDestroy { - @ViewChild('actionButton', {static: true}) actionButton: MatButton; + @ViewChild('actionButton') actionButton: MatButton; @HostBinding('class') get panelClass(): string[] { diff --git a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts index d7f4710f8d..a019f086dd 100644 --- a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts @@ -111,6 +111,7 @@ export const setValueActionTranslations = new Map( ); export enum ValueToDataType { + VALUE = 'VALUE', CONSTANT = 'CONSTANT', FUNCTION = 'FUNCTION', NONE = 'NONE' diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 7187dc8eaa..569b2bec8a 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -190,3 +190,15 @@ export enum EdgeInstructionsMethod { } export const edgeVersionAttributeKey = 'edgeVersion'; + +export enum EdgeConnectionEvent { + CONNECTED= 'CONNECTED', + DISCONNECTED = 'DISCONNECTED' +} + +export const EdgeConnectionEventTranslationMap = new Map( + [ + [EdgeConnectionEvent.CONNECTED, 'edge.connected'], + [EdgeConnectionEvent.DISCONNECTED, 'edge.disconnected'] + ] +); diff --git a/ui-ngx/src/app/shared/models/limited-api.models.ts b/ui-ngx/src/app/shared/models/limited-api.models.ts index 714441f0b2..d0e2e0cb76 100644 --- a/ui-ngx/src/app/shared/models/limited-api.models.ts +++ b/ui-ngx/src/app/shared/models/limited-api.models.ts @@ -24,7 +24,9 @@ export enum LimitedApi { WS_UPDATES_PER_SESSION = 'WS_UPDATES_PER_SESSION', CASSANDRA_QUERIES = 'CASSANDRA_QUERIES', TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT', - TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE' + TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE', + EDGE_EVENTS = 'EDGE_EVENTS', + EDGE_EVENTS_PER_EDGE = 'EDGE_EVENTS_PER_EDGE' } export const LimitedApiTranslationMap = new Map( @@ -38,6 +40,8 @@ export const LimitedApiTranslationMap = new Map( [LimitedApi.WS_UPDATES_PER_SESSION, 'api-limit.ws-updates-per-session'], [LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'], [LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'], - [LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'] + [LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'], + [LimitedApi.EDGE_EVENTS, 'api-limit.edge-events'], + [LimitedApi.EDGE_EVENTS_PER_EDGE, 'api-limit.edge-events-per-edge'], ] ); diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 9943bebf3d..ae945b243c 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -474,7 +474,9 @@ export enum NotificationType { API_USAGE_LIMIT = 'API_USAGE_LIMIT', NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION', RULE_NODE = 'RULE_NODE', - RATE_LIMITS = 'RATE_LIMITS' + RATE_LIMITS = 'RATE_LIMITS', + EDGE_CONNECTION = 'EDGE_CONNECTION', + EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE' } export const NotificationTypeIcons = new Map([ @@ -585,6 +587,18 @@ export const NotificationTemplateTypeTranslateMap = new Map([ @@ -612,6 +628,8 @@ export const TriggerTypeTranslationMap = new Map([ [TriggerType.API_USAGE_LIMIT, 'notification.trigger.api-usage-limit'], [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], [TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'], + [TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'], + [TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure'] ]); export interface NotificationUserSettings { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 625d957cff..5f4ca8f7be 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -538,7 +538,8 @@ export enum WidgetActionType { openDashboard = 'openDashboard', custom = 'custom', customPretty = 'customPretty', - mobileAction = 'mobileAction' + mobileAction = 'mobileAction', + openURL = 'openURL' } export enum WidgetMobileActionType { @@ -559,7 +560,8 @@ export const widgetActionTypeTranslationMap = new Map( [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], [ WidgetActionType.custom, 'widget-action.custom' ], [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ], - [ WidgetActionType.mobileAction, 'widget-action.mobile-action' ] + [ WidgetActionType.mobileAction, 'widget-action.mobile-action' ], + [ WidgetActionType.openURL, 'widget-action.open-URL' ] ] ); @@ -671,6 +673,7 @@ export interface WidgetAction extends CustomActionDescriptor { setEntityId?: boolean; stateEntityParamName?: string; mobileAction?: WidgetMobileActionDescriptor; + url?: string; } export interface WidgetActionDescriptor extends WidgetAction { diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md new file mode 100644 index 0000000000..712f2b45e7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md @@ -0,0 +1,57 @@ +#### Edge communication failure notification templatization + +
+
+ +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `edgeId` - the edge id as uuid string; +* `edgeName` - the name of the edge; +* `failureMsg` - the string representation of the failure, occurred on the Edge; + +Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${edgeName:upperCase}` +* `lowerCase`, for example - `${edgeName:lowerCase}` +* `capitalize`, for example - `${edgeName:capitalize}` + +
+ +##### Examples + +Let's assume the notification about the failing of processing connection to Edge. +The following template: + +```text +Edge '${edgeName}' communication failure occurred +{:copy-code} +``` + +will be transformed to: + +```text +Edge 'DatacenterEdge' communication failure occurred +``` + +
+ +The following template: + +```text +Failure message: '${failureMsg}' +{:copy-code} +``` + +will be transformed to: + +```text +Failure message: 'Failed to process edge connection!' +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md new file mode 100644 index 0000000000..37f0ec7573 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md @@ -0,0 +1,44 @@ +#### Edge connection notification templatization + +
+
+ +Notification subject and message fields support templatization. +The list of available templatization parameters depends on the template type. +See the available types and parameters below: + +Available template parameters: + +* `edgeId` - the edge id as uuid string; +* `edgeName` - the name of the edge; +* `eventType` - the string representation of the connectivity status: connected or disconnected; + +Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. +You may also modify the value of the parameter with one of the suffixes: + +* `upperCase`, for example - `${edgeName:upperCase}` +* `lowerCase`, for example - `${edgeName:lowerCase}` +* `capitalize`, for example - `${edgeName:capitalize}` + +
+ +##### Examples + +Let's assume the notification about the connecting Edge into the ThingsBoard. +The following template: + +```text +Edge '${edgeName}' is now ${eventType} +{:copy-code} +``` + +will be transformed to: + +```text +Edge 'DatacenterEdge' is now connected +``` + +
+ +
+
diff --git a/ui-ngx/src/assets/help/en_US/notification/rate_limits.md b/ui-ngx/src/assets/help/en_US/notification/rate_limits.md index 8f6319ad80..d314e681c2 100644 --- a/ui-ngx/src/assets/help/en_US/notification/rate_limits.md +++ b/ui-ngx/src/assets/help/en_US/notification/rate_limits.md @@ -11,7 +11,7 @@ Available template parameters: * `api` - rate-limited API label; one of: 'REST API requests', 'REST API requests per customer', 'transport messages', 'transport messages per device', 'Cassandra queries', 'WS updates per session', 'notification requests', 'notification requests per rule', - 'entity version creation', 'entity version load'; + 'entity version creation', 'entity version load', 'Edge events', 'Edge events per edge'; * `limitLevelEntityType` - entity type of the limit level entity, e.g. 'Tenant', 'Device', 'Notification rule', 'Customer', etc.; * `limitLevelEntityId` - id of the limit level entity; * `limitLevelEntityName` - name of the limit level entity; diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md index 0ccbbd9058..50301746c4 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md @@ -1,4 +1,4 @@ Fields templatization feature allows you to process the incoming messages with dynamic configuration by substitution of templates specified in the configuration fields with values from message or message metadata. -For more detailed information, please refer to the ThingsBoard [documentation](https://thingsboard.io/docs/user-guide/templatization/) +For more detailed information, please refer to the ThingsBoard [documentation](${siteBaseUrl}/docs/user-guide/templatization/) diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md index 296e5b735c..d9b5bdb0b7 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md @@ -8,4 +8,4 @@ Enable 'debug mode' for your rule node to see the messages that arrive in near real-time. -See Debugging for more information. \ No newline at end of file +See Debugging for more information. diff --git a/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md index a31eae72cf..0f6ab96736 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md @@ -15,7 +15,7 @@ JavaScript function computing **an array of Link names** to forward the incoming Should return an array of `string` values presenting **link names** that the Rule Engine should use to further route the incoming Message.
If the result is an empty array - message will not be routed to any Node and will be immediately -acknowledged. +acknowledged.
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/tbel/common_node_script_args.md b/ui-ngx/src/assets/help/en_US/rulenode/tbel/common_node_script_args.md index 296e5b735c..d9b5bdb0b7 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/tbel/common_node_script_args.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/tbel/common_node_script_args.md @@ -8,4 +8,4 @@ Enable 'debug mode' for your rule node to see the messages that arrive in near real-time. -See Debugging for more information. \ No newline at end of file +See Debugging for more information. diff --git a/ui-ngx/src/assets/help/en_US/rulenode/tbel/switch_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/tbel/switch_node_script_fn.md index 7fd3b7447b..aed694848e 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/tbel/switch_node_script_fn.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/tbel/switch_node_script_fn.md @@ -15,7 +15,7 @@ Should return an array of `string` values presenting **link names** that the Rule Engine should use to further route the incoming Message.
If the result is an empty array - message will not be routed to any Node and will be immediately -acknowledged. +acknowledged.
diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index dbe7141f97..5a2a7cbeeb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -67,10 +67,10 @@ "next-with-label": "Nächste: {{label}}", "read-more": "Mehr dazu", "hide": "Verstecken", - "done": "Erledigt", - "print": "Drucken", - "restore": "Wiederherstellen", - "confirm": "Bestätigen", + "done": "Erledigt", + "print": "Drucken", + "restore": "Wiederherstellen", + "confirm": "Bestätigen", "more": "Mehr", "less": "Weniger", "skip": "Überspringen", @@ -107,6 +107,23 @@ "base-url-required": "Basis-URL ist erforderlich.", "prohibit-different-url": "Prohibit to use hostname from the client request headers", "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", + "device-connectivity": { + "device-connectivity": "Geräte Konnektivität", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "Falls Host und Port leer sind, werden die Standardwerte des Protokolls verwendet", + "host": "Host", + "port": "Port", + "port-pattern": "Port muss einen positiven Wert haben", + "port-range": "Port sollte im Bereich 1 und 65535 liegen." + }, "mail-from": "E-Mail von", "mail-from-required": "E-Mail von ist erforderlich.", "smtp-protocol": "SMTP Protokoll", @@ -138,7 +155,16 @@ "mail-template": "E-Mail Vorlage", "test": "E-Mail Nachricht testen", "activation": "Nachricht Benutzerkontoaktivierung", - "account-activated": "Nachricht Benutzerkonto aktiviert" + "account-activated": "Nachricht Benutzerkonto aktiviert", + "account-lockout": "Nachricht zur Kontosperrung", + "reset-password": "Nachricht zum Zurücksetzen des Passworts", + "password-was-reset": "Nachricht für Passwort wurde zurückgesetzt", + "user-activated": "Nachricht für Benutzerkonto wurde aktiviert", + "user-registered": "Nachricht für Benutzer hat sich registriert", + "api-usage-state-enabled": "API-Nutzungsstatus wurde aktiviert", + "api-usage-state-warning": "Warnung zum API-Nutzungsstatus", + "api-usage-state-disabled": "API-Nutzungsstatus wurde deaktiviert", + "two-fa-verification": "2FA-Bestätigungsnachricht" }, "mail-subject": "E-Mail Betreff", "mail-body": "E-Mail Nachricht", @@ -186,17 +212,17 @@ "alarm-status-list": "Alarm Statusliste", "any-status": "Jeder Status", "search-status": { - "ANY": "Jeder", - "ACTIVE": "Aktiv", - "CLEARED": "Gelöscht", - "ACK": "Bestätigt", - "UNACK": "Nicht bestätigt" + "ANY": "Jeder", + "ACTIVE": "Aktiv", + "CLEARED": "Gelöscht", + "ACK": "Bestätigt", + "UNACK": "Nicht bestätigt" }, "display-status": { - "ACTIVE_UNACK": "Nicht bestätigt aktiv", - "ACTIVE_ACK": "Bestätigt aktiv", - "CLEARED_UNACK": "Nicht bestätigt", - "CLEARED_ACK": "Bestätigung gelöscht" + "ACTIVE_UNACK": "Nicht bestätigt aktiv", + "ACTIVE_ACK": "Bestätigt aktiv", + "CLEARED_UNACK": "Nicht bestätigt", + "CLEARED_ACK": "Bestätigung gelöscht" }, "no-alarms-prompt": "Keine Alarme gefunden", "created-time": "Erstellungszeit", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4160b50024..c71f33fca7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -856,7 +856,9 @@ "rest-api-requests-per-customer": "REST API requests per customer", "transport-messages": "Transport messages", "transport-messages-per-device": "Transport messages per device", - "ws-updates-per-session": "WS updates per session" + "ws-updates-per-session": "WS updates per session", + "edge-events": "Edge events", + "edge-events-per-edge": "Edge events per edge" }, "audit-log": { "audit": "Audit", @@ -2037,7 +2039,9 @@ "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", "upgrade-instructions": "Upgrade Instructions", - "widget-datasource-error": "This widget supports only EDGE entity datasource" + "widget-datasource-error": "This widget supports only EDGE entity datasource", + "connected": "Connected", + "disconnected": "Disconnected" }, "edge-event": { "type-dashboard": "Dashboard", @@ -3288,6 +3292,8 @@ "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", "disabled": "Disabled", + "edge-trigger-settings": "Edge trigger settings", + "edge-list-rule-hint": "If the field is empty, the trigger will be applied to all edge instances", "edit-notification-recipients-group": "Edit notification recipients group", "edit-notification-template": "Edit notification template", "edit-rule": "Edit rule", @@ -3432,7 +3438,9 @@ "rule-engine-lifecycle-event": "Rule engine lifecycle event", "rule-node": "Rule node", "new-platform-version": "New platform version", - "rate-limits": "Exceeded rate limits" + "rate-limits": "Exceeded rate limits", + "edge-communication-failure": "Edge communication failure", + "edge-connection": "Edge connection" }, "templates": "Templates", "notification-templates": "Notifications / Templates", @@ -3453,6 +3461,8 @@ "rule-engine-lifecycle-event": "Rule engine lifecycle event", "new-platform-version": "New platform version", "rate-limits": "Exceeded rate limits", + "edge-connection": "Edge connection", + "edge-communication-failure": "Edge communication failure", "trigger": "Trigger", "trigger-required": "Trigger is required" }, @@ -4185,6 +4195,10 @@ "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", "edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits", "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits", + "edit-edge-events-rate-limit": "Edit edge events rate limits", + "edit-edge-events-per-edge-rate-limit": "Edit edge events per edge rate limits", + "edge-events-rate-limit": "Edge events", + "edge-events-per-edge-rate-limit": "Edge events per edge", "messages-per": "messages per", "not-set": "Not set", "number-of-messages": "Number of messages", @@ -5022,6 +5036,9 @@ "popover-height": "Popover height", "popover-style": "Popover style", "open-new-browser-tab": "Open in a new browser tab", + "open-URL": "Open URL", + "URL": "URL", + "url-required": "URL is required.", "mobile": { "action-type": "Mobile action type", "select-action-type": "Select mobile action type", @@ -5185,13 +5202,35 @@ "action-button": { "behavior": "Behavior", "on-click": "On click", - "on-click-hint": "Action performed when the button is clicked." + "on-click-hint": "Action triggered when the button is clicked" }, "command-button": { "behavior": "Behavior", "on-click": "On click", "on-click-hint": "Action performed when the button is clicked." }, + "power-button": { + "behavior": "Behavior", + "power-on": "Power 'On'", + "power-on-hint": "Action performed to power ON the component.", + "power-off": "Power 'Off'", + "power-off-hint": "Action performed to power OFF the component.", + "on-label": "On", + "off-label": "Off", + "layout": "Layout", + "layout-default": "Default", + "layout-simplified": "Simplified", + "layout-outlined": "Outlined", + "layout-default-volume": "Default.Volume", + "layout-simplified-volume": "Simplified.Volume", + "layout-outlined-volume": "Outlined.Volume", + "main": "Main", + "background": "Background", + "power-on-colors": "Power 'On' colors", + "power-off-colors": "Power 'Off' colors", + "disabled-colors": "Disabled colors", + "button": "Button" + }, "button": { "layout": "Layout", "outlined": "Outlined", @@ -5214,9 +5253,9 @@ }, "button-state": { "activated-state": "Activated state", - "activated-state-hint": "Condition under which the button is active.", + "activated-state-hint": "Configure condition under which the button is active.", "disabled-state": "Disabled state", - "disabled-state-hint": "Condition under which the button is disabled.", + "disabled-state-hint": "Configure condition under which the button is disabled.", "enabled": "Enabled", "hovered": "Hovered", "pressed": "Pressed", @@ -6303,6 +6342,31 @@ "off-label": "Off label", "switch": "Switch" }, + "slider": { + "behavior": "Behavior", + "initial-value": "Initial value", + "initial-value-hint": "Action to get the initial value of the slider.", + "on-value-change": "On value change", + "on-value-change-hint": "Action triggered when the slider value is changed.", + "layout": "Layout", + "layout-default": "Default", + "layout-extended": "Extended", + "layout-simplified": "Simplified", + "auto-scale": "Auto scale", + "icon": "Icon", + "value": "Value", + "range": "Range", + "min": "min", + "max": "max", + "range-ticks": "Range ticks", + "tick-marks": "Tick marks", + "colors": "Colors", + "main": "Main", + "background": "Background", + "left-icon": "Left icon", + "right-icon": "Right icon", + "slider": "Slider" + }, "value-card": { "layout": "Layout", "layout-square": "Square", @@ -6325,6 +6389,7 @@ "layout": "Layout", "background-overlay": "Value background overlay", "total-volume": "Total volume", + "total-volume-units": "Total volume units", "tank": "Tank", "shape": "Shape", "datasource-units": "Source units", @@ -6526,13 +6591,13 @@ }, "rpc-state": { "initial-state": "Initial state", - "initial-state-hint": "Action to get the initial value of the component.", + "initial-state-hint": "Action to get the initial state (On/Off) of the component.", "disabled-state": "Disabled state", - "disabled-state-hint": "Condition under which the component is disabled.", + "disabled-state-hint": "Configure condition under which the component is disabled.", "turn-on": "Turn 'On'", - "turn-on-hint": "Action performed to turn ON the component.", + "turn-on-hint": "Action triggered when the slider is switched to 'On'", "turn-off": "Turn 'Off'", - "turn-off-hint": "Action performed to turn OFF the component.", + "turn-off-hint": "Action triggered when the slider is switched to 'Off'", "on": "On", "off": "Off", "disabled": "Disabled" @@ -6573,6 +6638,7 @@ "converter-none": "None", "converter-function": "Function", "converter-constant": "Constant", + "converter-value": "Value", "parse-value-function": "Parse value function", "state-when-result-is": "'{{state}}' when result is", "parameters": "Parameters", @@ -6859,6 +6925,7 @@ "ko_KR": "한국어", "lv_LV": "Latviešu", "nl_BE": "Koninkrijk België", + "pl_PL": "Polski", "pt_BR": "Português do Brasil", "ro_RO": "Română", "sl_SI": "Slovenščina", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json new file mode 100644 index 0000000000..014666017c --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -0,0 +1,6703 @@ +{ + "access":{ + "unauthorized":"Unauthorized", + "unauthorized-access":"Unauthorized Access", + "unauthorized-access-text":"You should sign in to have access to this resource!", + "access-forbidden":"Access Forbidden", + "access-forbidden-text":"You haven't access rights to this location!
Try to sign in with different user if you still wish to gain access to this location.", + "refresh-token-expired":"Session has expired", + "refresh-token-failed":"Unable to refresh session", + "permission-denied":"Permission Denied", + "permission-denied-text":"You don't have permission to perform this operation!" + }, + "account":{ + "account":"Konto", + "notification-settings":"Ustawienia powiadomień" + }, + "action":{ + "activate":"Aktywuj", + "suspend":"Zawieś", + "save":"Zapisz", + "saveAs":"Zapisz jako", + "move":"Przenieś", + "cancel":"Anuluj", + "ok":"OK", + "delete":"Usuń", + "add":"Dodaj", + "yes":"Tak", + "no":"Nie", + "update":"Aktualizuj", + "remove":"Usuń", + "select":"Wybierz", + "search":"Szukaj", + "clear-search":"Wyczyść wyszukiwanie", + "assign":"Przypisz", + "unassign":"Cofnij przypisanie", + "share":"Udostępnij", + "make-private":"Uczyń prywatnym", + "apply":"Zastosuj", + "apply-changes":"Zastosuj zmiany", + "edit-mode":"Tryb edycji", + "enter-edit-mode":"Wejdź w tryb edycji", + "decline-changes":"Odrzuć zmiany", + "decline":"Odrzuć", + "close":"Zamknij", + "back":"Wstecz", + "run":"Uruchom", + "sign-in":"Zaloguj się!", + "edit":"Edytuj", + "view":"Zobacz", + "create":"Utwórz", + "drag":"Przeciągnij", + "refresh":"Odśwież", + "undo":"Cofnij", + "copy":"Kopiuj", + "paste":"Wklej", + "copy-reference":"Kopiuj odniesienie", + "paste-reference":"Wklej odniesienie", + "import":"Importuj", + "export":"Eksportuj", + "share-via":"Udostępnij przez {{provider}}", + "continue":"Kontynuuj", + "discard-changes":"Porzuć zmiany", + "download":"Pobierz", + "next":"Dalej", + "next-with-label":"Następne: {{label}}", + "read-more":"Czytaj więcej", + "hide":"Ukryj", + "done":"Gotowe", + "print":"Drukuj", + "restore":"Przywróć", + "confirm":"Potwierdź", + "more":"Więcej", + "less":"Mniej", + "skip":"Pomiń", + "send":"Wyślij", + "reset":"Resetuj", + "show-more":"Pokaż więcej", + "dont-show-again":"Nie pokazuj ponownie", + "see-documentation":"Zobacz dokumentację", + "clear":"Wyczyść", + "upload":"Prześlij", + "delete-anyway":"Usuń mimo wszystko", + "delete-selected":"Usuń wybrane" + }, + "aggregation":{ + "aggregation":"Agregacja", + "function":"Funkcja agregacji danych", + "limit":"Maksymalna liczba wartości", + "group-interval":"Interwał grupowania", + "min":"Min", + "max":"Maks", + "avg":"Średnia", + "sum":"Suma", + "count":"Liczba", + "none":"Brak" + }, + "admin":{ + "settings":"Ustawienia", + "general":"Ogólne", + "general-settings":"Ustawienia Ogólne", + "home-settings":"Ustawienia Strony Głównej", + "home":"Strona Główna", + "outgoing-mail":"Serwer poczty", + "outgoing-mail-settings":"Ustawienia Serwera Poczty Wychodzącej", + "system-settings":"Ustawienia Systemowe", + "test-mail-sent":"Testowy email został wysłany pomyślnie!", + "base-url":"Podstawowy URL", + "base-url-required":"Podstawowy URL jest wymagany.", + "prohibit-different-url":"Zabroń używania nazwy hosta z nagłówków żądania klienta", + "prohibit-different-url-hint":"Ta opcja powinna być włączona w środowiskach produkcyjnych. Może powodować problemy bezpieczeństwa, gdy jest wyłączona", + "device-connectivity":{ + "device-connectivity":"Łączność urządzenia", + "http-s":"HTTP(s)", + "mqtt-s":"MQTT(s)", + "coap-s":"COAP(s)", + "http":"HTTP", + "https":"HTTPs", + "mqtt":"MQTT", + "mqtts":"MQTTs", + "coap":"COAP", + "coaps":"COAPs", + "hint":"Jeśli pola hosta lub portu są puste, zostanie użyta domyślna wartość protokołu.", + "host":"Host", + "port":"Port", + "port-pattern":"Port musi być dodatnią liczbą całkowitą.", + "port-range":"Port powinien mieścić się w zakresie od 1 do 65535." + }, + "mail-from":"Nadawca", + "mail-from-required":"Nadawca jest wymagany.", + "smtp-protocol":"Protokół SMTP", + "smtp-host":"Host SMTP", + "smtp-host-required":"Host SMTP jest wymagany.", + "smtp-port":"Port SMTP", + "smtp-port-required":"Musisz podać port SMTP.", + "smtp-port-invalid":"To nie wygląda na prawidłowy port SMTP.", + "timeout-msec":"Limit czasu (ms)", + "timeout-required":"Limit czasu jest wymagany.", + "timeout-invalid":"To nie wygląda na prawidłowy limit czasu.", + "enable-tls":"Włącz TLS", + "tls-version":"Wersja TLS", + "enable-proxy":"Włącz proxy", + "proxy-host":"Host proxy", + "proxy-host-required":"Host proxy jest wymagany.", + "proxy-port":"Port proxy", + "proxy-port-required":"Port proxy jest wymagany.", + "proxy-port-range":"Port proxy powinien znajdować się w zakresie od 1 do 65535.", + "proxy-user":"Użytkownik proxy", + "proxy-password":"Hasło proxy", + "change-password":"Zmień hasło", + "send-test-mail":"Wyślij mail testowy", + "sms-provider":"Dostawca SMS", + "sms-provider-settings":"Ustawienia dostawcy SMS", + "sms-provider-type":"Typ dostawcy SMS", + "sms-provider-type-required":"Typ dostawcy SMS jest wymagany.", + "sms-provider-type-aws-sns":"Amazon SNS", + "sms-provider-type-twilio":"Twilio", + "sms-provider-type-smpp":"SMPP", + "aws-access-key-id":"ID klucza dostępu AWS", + "aws-access-key-id-required":"ID klucza dostępu AWS jest wymagane", + "aws-secret-access-key":"Tajny klucz dostępu AWS", + "aws-secret-access-key-required":"Tajny klucz dostępu AWS jest wymagany", + "aws-region":"Region AWS", + "aws-region-required":"Region AWS jest wymagany", + "number-from":"Numer telefonu nadawcy", + "number-from-required":"Numer telefonu nadawcy jest wymagany.", + "number-to":"Numer telefonu odbiorcy", + "number-to-required":"Numer telefonu odbiorcy jest wymagany.", + "phone-number-hint":"Numer telefonu w formacie E.164, np. +19995550123", + "phone-number-hint-twilio":"Numer telefonu w formacie E.164/Numer SID telefonu/SID usługi wiadomości, np. +19995550123/PNXXX/MGXXX", + "phone-number-pattern":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. +19995550123.", + "phone-number-pattern-twilio":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164/Numer SID telefonu/SID usługi wiadomości, np. +19995550123/PNXXX/MGXXX.", + "sms-message":"Wiadomość SMS", + "sms-message-required":"Wiadomość SMS jest wymagana.", + "sms-message-max-length":"Wiadomość SMS nie może być dłuższa niż 1600 znaków", + "twilio-account-sid":"SID konta Twilio", + "twilio-account-sid-required":"SID konta Twilio jest wymagany", + "twilio-account-token":"Token konta Twilio", + "twilio-account-token-required":"Token konta Twilio jest wymagany", + "send-test-sms":"Wyślij testowego SMS-a", + "test-sms-sent":"Testowy SMS został wysłany pomyślnie!", + "security-settings":"Ustawienia bezpieczeństwa", + "password-policy":"Polityka haseł", + "minimum-password-length":"Minimalna długość hasła", + "minimum-password-length-required":"Minimalna długość hasła jest wymagana", + "minimum-password-length-range":"Minimalna długość hasła powinna wynosić od 6 do 50", + "maximum-password-length":"Maksymalna długość hasła", + "maximum-password-length-min":"Maksymalna długość hasła powinna wynosić co najmniej 6", + "maximum-password-length-less-min":"Maksymalna długość hasła powinna być większa niż minimalna długość", + "minimum-uppercase-letters":"Minimalna liczba wielkich liter", + "minimum-uppercase-letters-range":"Minimalna liczba wielkich liter nie może być ujemna", + "minimum-lowercase-letters":"Minimalna liczba małych liter", + "minimum-lowercase-letters-range":"Minimalna liczba małych liter nie może być ujemna", + "minimum-digits":"Minimalna liczba cyfr", + "minimum-digits-range":"Minimalna liczba cyfr nie może być ujemna", + "minimum-special-characters":"Minimalna liczba znaków specjalnych", + "minimum-special-characters-range":"Minimalna liczba znaków specjalnych nie może być ujemna", + "password-expiration-period-days":"Okres ważności hasła w dniach", + "password-expiration-period-days-range":"Okres ważności hasła w dniach nie może być ujemny", + "password-reuse-frequency-days":"Częstotliwość ponownego używania hasła w dniach", + "password-reuse-frequency-days-range":"Częstotliwość ponownego używania hasła w dniach nie może być ujemna", + "allow-whitespace":"Zezwól na białe znaki", + "force-reset-password-if-no-valid":"Wymuś zresetowanie hasła, jeśli nie jest ważne", + "force-reset-password-if-no-valid-hint":"Ostrożnie z włączeniem tej funkcji: będzie wymagać od użytkowników z nieaktualnymi hasłami resetowania ich poprzez email.", + "general-policy":"Ogólna polityka", + "max-failed-login-attempts":"Maksymalna liczba nieudanych prób", + "minimum-max-failed-login-attempts-range":"Maksymalna liczba nieudanych prób logowania nie może być ujemna", + "user-lockout-notification-email":"W przypadku zablokowania konta użytkownika, wyślij powiadomienie na email", + "domain-name":"Nazwa domeny", + "domain-name-unique":"Nazwa domeny i protokół muszą być unikalne.", + "domain-name-max-length":"Nazwa domeny powinna być krótsza niż 256", + "error-verification-url":"Nazwa domeny nie powinna zawierać symboli '/' i ':'. Przykład: thingsboard.io", + "connection-settings":"Ustawienia połączenia", + "oauth2":{ + "access-token-uri":"URI tokenu dostępu", + "access-token-uri-required":"URI tokenu dostępu jest wymagane.", + "activate-user":"Aktywuj użytkownika", + "add-domain":"Dodaj domenę", + "delete-domain":"Usuń domenę", + "add-provider":"Dodaj dostawcę", + "delete-provider":"Usuń dostawcę", + "allow-user-creation":"Zezwalaj na tworzenie użytkowników", + "always-fullscreen":"Zawsze na pełnym ekranie", + "authorization-uri":"URI autoryzacji", + "authorization-uri-required":"URI autoryzacji jest wymagane.", + "client-authentication-method":"Metoda uwierzytelnienia klienta", + "client-id":"ID klienta", + "client-id-required":"ID klienta jest wymagane.", + "client-id-max-length":"ID klienta powinno być krótsze niż 256", + "client-secret":"Sekret klienta", + "client-secret-required":"Sekret klienta jest wymagany.", + "client-secret-max-length":"Sekret klienta powinien być krótszy niż 2049", + "custom-setting":"Niestandardowe ustawienia", + "customer-name-pattern":"Wzorzec nazwy klienta", + "customer-name-pattern-max-length":"Wzorzec nazwy klienta powinien być krótszy niż 256", + "default-dashboard-name":"Domyślna nazwa pulpitu nawigacyjnego", + "default-dashboard-name-max-length":"Domyślna nazwa pulpitu nawigacyjnego powinna być krótsza niż 256", + "delete-domain-text":"Uważaj, po potwierdzeniu domena i wszystkie dane dostawcy będą niedostępne.", + "delete-domain-title":"Czy na pewno chcesz usunąć ustawienia domeny '{{domainName}}'?", + "delete-registration-text":"Uważaj, po potwierdzeniu dane dostawcy będą niedostępne.", + "delete-registration-title":"Czy na pewno chcesz usunąć dostawcę '{{name}}'?", + "email-attribute-key":"Klucz atrybutu email", + "email-attribute-key-required":"Klucz atrybutu email jest wymagany.", + "email-attribute-key-max-length":"Klucz atrybutu email powinien być krótszy niż 32", + "first-name-attribute-key":"Klucz atrybutu imienia", + "first-name-attribute-key-max-length":"Klucz atrybutu imienia powinien być krótszy niż 32", + "general":"Ogólne", + "jwk-set-uri":"URI klucza Web JSON", + "last-name-attribute-key":"Klucz atrybutu nazwiska", + "last-name-attribute-key-max-length":"Klucz atrybutu nazwiska powinien być krótszy niż 32", + "login-button-icon":"Ikona przycisku logowania", + "login-button-label":"Etykieta dostawcy", + "login-button-label-placeholder":"Zaloguj się przez $(Etykieta dostawcy)", + "login-button-label-required":"Etykieta jest wymagana.", + "login-provider":"Dostawca logowania", + "mapper":"Mapper", + "new-domain":"Nowa domena", + "oauth2":"OAuth2", + "password-max-length":"Hasło powinno być krótsze niż 256", + "redirect-uri-template":"Szablon URI przekierowania", + "copy-redirect-uri":"Kopiuj URI przekierowania", + "registration-id":"ID rejestracji", + "registration-id-required":"ID rejestracji jest wymagane.", + "registration-id-unique":"ID rejestracji musi być unikalne dla systemu.", + "scope":"Zakres", + "scope-required":"Zakres jest wymagany.", + "tenant-name-pattern":"Wzorzec nazwy dzierżawcy", + "tenant-name-pattern-required":"Wzorzec nazwy dzierżawcy jest wymagany.", + "tenant-name-pattern-max-length":"Wzorzec nazwy dzierżawcy powinien być krótszy niż 256", + "tenant-name-strategy":"Strategia nazewnictwa dzierżawcy", + "type":"Typ mappera", + "uri-pattern-error":"Nieprawidłowy format URI.", + "url":"URL", + "url-pattern":"Nieprawidłowy format URL.", + "url-required":"URL jest wymagany.", + "url-max-length":"URL powinien być krótszy niż 256", + "user-info-uri":"URI informacji o użytkowniku", + "user-info-uri-required":"URI informacji o użytkowniku jest wymagane.", + "username-max-length":"Nazwa użytkownika powinna być krótsza niż 256", + "user-name-attribute-name":"Klucz atrybutu nazwy użytkownika", + "user-name-attribute-name-required":"Klucz atrybutu nazwy użytkownika jest wymagany", + "protocol":"Protokół", + "domain-schema-http":"HTTP", + "domain-schema-https":"HTTPS", + "domain-schema-mixed":"HTTP+HTTPS", + "enable":"Włącz ustawienia OAuth2", + "domains":"Domeny", + "mobile-apps":"Aplikacje mobilne", + "no-mobile-apps":"Brak skonfigurowanych aplikacji", + "mobile-package":"Pakiet aplikacji", + "mobile-package-placeholder":"Np.: my.example.app", + "mobile-package-hint":"Dla Androida: unikalny ID aplikacji. Dla iOS: identyfikator paczki produktu.", + "mobile-package-unique":"Pakiet aplikacji musi być unikalny.", + "mobile-app-secret":"Sekret aplikacji", + "invalid-mobile-app-secret":"Sekret aplikacji musi zawierać tylko znaki alfanumeryczne i mieć od 16 do 2048 znaków.", + "copy-mobile-app-secret":"Kopiuj sekret aplikacji", + "add-mobile-app":"Dodaj aplikację", + "delete-mobile-app":"Usuń informacje o aplikacji", + "providers":"Dostawcy", + "platform-web":"Web", + "platform-android":"Android", + "platform-ios":"iOS", + "all-platforms":"Wszystkie platformy", + "smtp-provider":"Dostawca SMTP", + "allowed-platforms":"Dozwolone platformy", + "authentication":"Autentykacja", + "basic":"Podstawowy", + "provider":"Dostawca", + "redirect-url":"URI przekierowania", + "domain-name":"Nazwa domeny", + "redirect-url-template":"Szablon URI przekierowania", + "microsoft-tenant-id":"ID dzierżawy (tenant) Microsoft", + "microsoft-tenant-id-required":"ID dzierżawy (tenant) Microsoft jest wymagane", + "token-uri":"URI tokenu", + "token-uri-required":"URI tokenu jest wymagane", + "redirect-uri":"URI przekierowania", + "google-provider":"Google", + "microsoft-provider":"Office 365", + "sendgrid-provider":"Sendgrid", + "custom-provider":"Niestandardowy", + "generate-access-token":"Generuj token dostępu", + "update-access-token":"Aktualizuj token dostępu", + "access-token-status":"Status tokenu dostępu:", + "token-status-generated":"wygenerowany", + "token-status-not-generated":"nie wygenerowany" + }, + "smpp-provider":{ + "smpp-version":"Wersja SMPP", + "smpp-host":"Host SMPP", + "smpp-host-required":"Host SMPP jest wymagany", + "smpp-port":"Port SMPP", + "smpp-port-required":"Port SMPP jest wymagany", + "system-id":"ID systemu", + "system-id-required":"ID systemu jest wymagane", + "password":"Hasło", + "password-required":"Hasło jest wymagane", + "type-settings":"Ustawienia typu", + "source-settings":"Ustawienia źródła", + "destination-settings":"Ustawienia docelowe", + "additional-settings":"Dodatkowe ustawienia", + "system-type":"Typ systemu", + "bind-type":"Typ powiązania", + "service-type":"Typ usługi", + "source-address":"Adres źródłowy", + "source-ton":"TON źródłowy", + "source-npi":"NPI źródłowy", + "destination-ton":"TON docelowy (Typ Numeru)", + "destination-npi":"NPI docelowy (Identyfikacja Planu Numeracji)", + "address-range":"Zakres adresów", + "coding-scheme":"Schemat kodowania", + "bind-type-tx":"Nadajnik", + "bind-type-rx":"Odbiornik", + "bind-type-trx":"Nadawczo-odbiorczy", + "ton-unknown":"Nieznany", + "ton-international":"Międzynarodowy", + "ton-national":"Krajowy", + "ton-network-specific":"Specyficzny dla sieci", + "ton-subscriber-number":"Numer abonenta", + "ton-alphanumeric":"Alfanumeryczny", + "ton-abbreviated":"Skrócony", + "npi-unknown":"0 - Nieznany", + "npi-isdn":"1 - ISDN/plan numeracji telefonicznej (E163/E164)", + "npi-data-numbering-plan":"3 - Plan numeracji danych (X.121)", + "npi-telex-numbering-plan":"4 - Plan numeracji telexu (F.69)", + "npi-land-mobile":"6 - Mobilny lądowy (E.212)", + "npi-national-numbering-plan":"8 - Krajowy plan numeracji", + "npi-private-numbering-plan":"9 - Prywatny plan numeracji", + "npi-ermes-numbering-plan":"10 - Plan numeracji ERMES (ETSI DE/PS 3 01-3)", + "npi-internet":"13 - Internet (IP)", + "npi-wap-client-id":"18 - Identyfikator klienta WAP (do zdefiniowania przez Forum WAP)", + "scheme-smsc":"0 - Domyślny alfabet SMSC (ASCII dla krótkiego i długiego kodu oraz do GSM dla bezpłatnych)", + "scheme-ia5":"1 - IA5 (ASCII dla krótkiego i długiego kodu, łaciński 9 dla bezpłatnych (ISO-8859-9))", + "scheme-octet-unspecified-2":"2 - Oktet nieokreślony (binarny 8-bitowy)", + "scheme-latin-1":"3 - Łaciński 1 (ISO-8859-1)", + "scheme-octet-unspecified-4":"4 - Oktet nieokreślony (binarny 8-bitowy)", + "scheme-jis":"5 - JIS (X 0208-1990)", + "scheme-cyrillic":"6 - Cyrylica (ISO-8859-5)", + "scheme-latin-hebrew":"7 - Łaciński/hebrajski (ISO-8859-8)", + "scheme-ucs-utf":"8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding":"9 - Kodowanie piktogramów", + "scheme-music-codes":"10 - Kody muzyczne (ISO-2022-JP)", + "scheme-extended-kanji-jis":"13 - Rozszerzony Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set":"14 - Zestaw koreańskich znaków graficznych (KS C 5601/KS X 1001)" + }, + "queue-select-name":"Wybierz nazwę kolejki", + "queue-name":"Nazwa", + "queue-name-required":"Nazwa kolejki jest wymagana!", + "queues":"Kolejki", + "queue-partitions":"Partycje", + "queue-submit-strategy":"Strategia zgłaszania", + "queue-processing-strategy":"Strategia przetwarzania", + "queue-configuration":"Konfiguracja kolejki", + "repository-settings":"Ustawienia repozytorium", + "repository":"Repozytorium", + "repository-url":"URL repozytorium", + "repository-url-required":"URL repozytorium jest wymagany.", + "default-branch":"Domyślna nazwa gałęzi", + "repository-read-only":"Tylko do odczytu", + "show-merge-commits":"Pokaż commity scalające", + "authentication-settings":"Ustawienia autentykacji", + "auth-method":"Metoda uwierzytelniania", + "auth-method-username-password":"Hasło / token dostępu", + "auth-method-username-password-hint":"Użytkownicy GitHub muszą używać tokenów dostępu z uprawnieniami do zapisu w repozytorium.", + "auth-method-private-key":"Klucz prywatny", + "password-access-token":"Hasło / token dostępu", + "change-password-access-token":"Zmień hasło / token dostępu", + "private-key":"Klucz prywatny", + "drop-private-key-file-or":"Przeciągnij i upuść plik klucza prywatnego lub", + "passphrase":"Hasło", + "enter-passphrase":"Wprowadź hasło", + "change-passphrase":"Zmień hasło", + "check-access":"Sprawdź dostęp", + "check-repository-access-success":"Dostęp do repozytorium pomyślnie zweryfikowany!", + "delete-repository-settings-title":"Czy na pewno chcesz usunąć ustawienia repozytorium?", + "delete-repository-settings-text":"Uważaj, po potwierdzeniu ustawienia repozytorium zostaną usunięte, a funkcja kontroli wersji będzie niedostępna.", + "auto-commit-settings":"Ustawienia automatycznego zatwierdzania", + "auto-commit":"Automatyczne zatwierdzanie", + "auto-commit-entities":"Automatyczne zatwierdzanie encji", + "no-auto-commit-entities-prompt":"Brak skonfigurowanych encji do automatycznego zatwierdzania", + "delete-auto-commit-settings-title":"Czy na pewno chcesz usunąć ustawienia automatycznego zatwierdzania?", + "delete-auto-commit-settings-text":"Uważaj, po potwierdzeniu ustawienia automatycznego zatwierdzania zostaną usunięte, a automatyczne zatwierdzanie zostanie wyłączone dla wszystkich encji.", + "2fa":{ + "2fa":"Dwuetapowa autentykacja", + "available-providers":"Dostępni dostawcy", + "issuer-name":"Nazwa wydawcy", + "issuer-name-required":"Nazwa wydawcy jest wymagana.", + "max-verification-failures-before-user-lockout":"Maksymalna liczba nieudanych weryfikacji przed zablokowaniem użytkownika", + "max-verification-failures-before-user-lockout-pattern":"Maksymalna liczba nieudanych weryfikacji musi być dodatnią liczbą całkowitą.", + "number-of-checking-attempts":"Liczba prób sprawdzenia", + "number-of-checking-attempts-pattern":"Liczba prób sprawdzenia musi być dodatnią liczbą całkowitą.", + "number-of-checking-attempts-required":"Liczba prób sprawdzenia jest wymagana.", + "number-of-codes":"Liczba kodów", + "number-of-codes-pattern":"Liczba kodów musi być dodatnią liczbą całkowitą.", + "number-of-codes-required":"Liczba kodów jest wymagana.", + "provider":"Dostawca", + "retry-verification-code-period":"Okres ponownego próbowania kodu weryfikacyjnego (sek)", + "retry-verification-code-period-pattern":"Minimalny czas okresu to 5 sek", + "retry-verification-code-period-required":"Okres ponownego próbowania kodu weryfikacyjnego jest wymagany.", + "total-allowed-time-for-verification":"Całkowity dozwolony czas na weryfikację (sek)", + "total-allowed-time-for-verification-pattern":"Minimalny całkowity dozwolony czas to 60 sek", + "total-allowed-time-for-verification-required":"Całkowity dozwolony czas jest wymagany.", + "use-system-two-factor-auth-settings":"Użyj systemowych ustawień dwuetapowej autentykacji", + "verification-code-check-rate-limit":"Limit częstotliwości sprawdzania kodu weryfikacyjnego", + "verification-code-lifetime":"Czas życia kodu weryfikacyjnego (sek)", + "verification-code-lifetime-pattern":"Czas życia kodu weryfikacyjnego musi być dodatnią liczbą całkowitą.", + "verification-code-lifetime-required":"Czas życia kodu weryfikacyjnego jest wymagany.", + "verification-message-template":"Szablon wiadomości weryfikacyjnej", + "verification-limitations":"Ograniczenia weryfikacji", + "verification-message-template-pattern":"Wiadomość weryfikacyjna musi zawierać wzór: ${code}", + "verification-message-template-required":"Szablon wiadomości weryfikacyjnej jest wymagany.", + "within-time":"W czasie (sek)", + "within-time-pattern":"Czas musi być dodatnią liczbą całkowitą.", + "within-time-required":"Czas jest wymagany." + }, + "jwt":{ + "security-settings":"Ustawienia bezpieczeństwa JWT", + "issuer-name":"Nazwa wydawcy", + "issuer-name-required":"Nazwa wydawcy jest wymagana.", + "signings-key":"Klucz podpisujący", + "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 256 bitów danych.", + "signings-key-required":"Klucz podpisujący jest wymagany.", + "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 256 bitów danych.", + "signings-key-base64":"Klucz podpisujący musi być w formacie base64.", + "expiration-time":"Czas wygaśnięcia tokena (sek)", + "expiration-time-required":"Czas wygaśnięcia tokena jest wymagany.", + "expiration-time-pattern":"Czas wygaśnięcia tokena musi być dodatnią liczbą całkowitą.", + "expiration-time-min":"Minimalny czas to 60 sekund (1 minuta).", + "refresh-expiration-time":"Czas wygaśnięcia tokena odświeżającego (sek)", + "refresh-expiration-time-required":"Czas wygaśnięcia tokena odświeżającego jest wymagany.", + "refresh-expiration-time-pattern":"Czas wygaśnięcia tokena odświeżającego musi być dodatnią liczbą całkowitą.", + "refresh-expiration-time-min":"Minimalny czas to 900 sekund (15 minut).", + "refresh-expiration-time-less-token":"Czas tokena odświeżającego musi być dłuższy niż czas tokena.", + "generate-key":"Wygeneruj klucz", + "info-header":"Wszyscy użytkownicy będą musieli ponownie się zalogować", + "info-message":"Zmiana klucza podpisującego JWT spowoduje unieważnienie wszystkich wydanych tokenów. Wszyscy użytkownicy będą musieli ponownie się zalogować. Dotyczy to również skryptów korzystających z Rest API/Websockets." + }, + "resources":"Zasoby", + "notifications":"Powiadomienia", + "notifications-settings":"Ustawienia powiadomień", + "slack-api-token":"Token API Slack", + "slack":"Slack", + "slack-settings":"Ustawienia Slack" + }, + "alarm":{ + "alarm":"Alarm", + "alarms":"Alarmy", + "all-alarms":"Wszystkie alarmy", + "select-alarm":"Wybierz alarm", + "no-alarms-matching":"Nie znaleziono alarmów pasujących do '{{entity}}'.", + "alarm-required":"Alarm jest wymagany", + "alarm-filter":"Filtr alarmów", + "filter":"Filtr", + "alarm-status":"Status alarmu", + "alarm-status-list":"Lista statusów alarmów", + "any-status":"Dowolny status", + "search-status":{ + "ANY":"Dowolny", + "ACTIVE":"Aktywny", + "CLEARED":"Wyczyszczony", + "ACK":"Potwierdzony", + "UNACK":"Niepotwierdzony" + }, + "display-status":{ + "ACTIVE_UNACK":"Aktywny Niepotwierdzony", + "ACTIVE_ACK":"Aktywny Potwierdzony", + "CLEARED_UNACK":"Wyczyszczony Niepotwierdzony", + "CLEARED_ACK":"Wyczyszczony Potwierdzony" + }, + "no-alarms-prompt":"Nie znaleziono alarmów", + "created-time":"Czas utworzenia", + "type":"Typ", + "severity":"Waga", + "originator":"Inicjator", + "originator-type":"Typ inicjatora", + "details":"Szczegóły", + "originator-label":"Etykieta inicjatora", + "assign":"Przypisz", + "assignments":"Przypisania", + "assignee":"Przypisany", + "assignee-id":"ID przypisanego", + "assignee-first-name":"Imię przypisanego", + "assignee-last-name":"Nazwisko przypisanego", + "assignee-email":"Email przypisanego", + "unassigned":"Nieprzypisany", + "assignee-not-set":"Wszystkie", + "status":"Status", + "alarm-details":"Szczegóły alarmu", + "start-time":"Czas rozpoczęcia", + "assign-time":"Czas przypisania", + "end-time":"Czas zakończenia", + "ack-time":"Czas potwierdzenia", + "clear-time":"Czas wyczyszczenia", + "duration":"Czas trwania", + "alarm-severity-list":"Lista ważności alarmów", + "any-severity":"Dowolna ważność", + "severity-critical":"Krytyczna", + "severity-major":"Poważna", + "severity-minor":"Mniejsza", + "severity-warning":"Ostrzeżenie", + "severity-indeterminate":"Nieokreślona", + "acknowledge":"Potwierdź", + "clear":"Wyczyść", + "delete":"Usuń", + "search":"Szukaj alarmów", + "selected-alarms":"{ count, plural, =1 {1 alarm} other {# alarmów} } wybrano", + "no-data":"Brak danych do wyświetlenia", + "polling-interval":"Interwał odpytywania alarmów (sek)", + "polling-interval-required":"Interwał odpytywania alarmów jest wymagany.", + "min-polling-interval-message":"Dozwolony jest minimalny interwał odpytywania 1 sek.", + "aknowledge-alarms-title":"Potwierdź { count, plural, =1 {1 alarm} other {# alarmów} }", + "aknowledge-alarms-text":"Czy na pewno chcesz potwierdzić { count, plural, =1 {1 alarm} other {# alarmów} }?", + "aknowledge-alarm-title":"Potwierdź alarm", + "aknowledge-alarm-text":"Czy na pewno chcesz potwierdzić alarm?", + "selected-alarms-are-acknowledged":"Wybrane alarmy zostały już potwierdzone", + "clear-alarms-title":"Wyczyść { count, plural, =1 {1 alarm} other {# alarmów} }", + "clear-alarms-text":"Czy na pewno chcesz wyczyścić { count, plural, =1 {1 alarm} other {# alarmów} }?", + "clear-alarm-title":"Wyczyść alarm", + "clear-alarm-text":"Czy na pewno chcesz wyczyścić alarm?", + "delete-alarms-title":"Usuń { count, plural, =1 {1 alarm} other {# alarmów} }", + "delete-alarms-text":"Czy na pewno chcesz usunąć { count, plural, =1 {1 alarm} other {# alarmów} }?", + "selected-alarms-are-cleared":"Wybrane alarmy zostały już wyczyszczone", + "alarm-status-filter":"Filtr statusu alarmów", + "alarm-filter-title":"Filtr alarmów", + "assigned":"Przypisane", + "filter-title":"Filtr", + "max-count-load":"Maksymalna liczba alarmów do załadowania (0 - bez limitu)", + "max-count-load-required":"Maksymalna liczba alarmów do załadowania jest wymagana.", + "max-count-load-error-min":"Minimalna wartość to 0.", + "fetch-size":"Rozmiar pobrania", + "fetch-size-required":"Rozmiar pobrania jest wymagany.", + "fetch-size-error-min":"Minimalna wartość to 10.", + "alarm-type-list":"Lista typów alarmów", + "any-type":"Dowolny typ", + "assigned-to-current-user":"Przypisane do aktualnego użytkownika", + "assigned-to-me":"Przypisane do mnie", + "search-propagated-alarms":"Szukaj propagowanych alarmów", + "comments":"Komentarze do alarmu", + "show-more":"Pokaż więcej", + "additional-info":"Dodatkowe informacje", + "alarm-type":"Typ alarmu", + "enter-alarm-type":"Wpisz typ alarmu", + "no-alarm-types-matching":"Nie znaleziono typów alarmów pasujących do '{{entitySubtype}}'.", + "alarm-type-list-empty":"Nie wybrano typów alarmów." + }, + "alarm-activity":{ + "add":"Dodaj komentarz...", + "alarm-comment":"Komentarz do alarmu", + "comments":"Komentarze", + "delete-alarm-comment":"Czy chcesz usunąć ten komentarz?", + "refresh":"Odśwież", + "oldest-first":"Najstarsze najpierw", + "newest-first":"Najnowsze najpierw", + "activity":"Aktywność", + "export":"Eksportuj do CSV", + "author":"Autor", + "created-date":"Data utworzenia", + "edited-date":"Data edycji", + "text":"Tekst", + "system":"System" + }, + "alias":{ + "add":"Dodaj alias", + "edit":"Edytuj alias", + "name":"Nazwa aliasu", + "name-required":"Nazwa aliasu jest wymagana", + "duplicate-alias":"Alias o tej samej nazwie już istnieje.", + "filter-type-single-entity":"Pojedyncza encja", + "filter-type-entity-list":"Lista encji", + "filter-type-entity-name":"Nazwa encji", + "filter-type-entity-type":"Typ encji", + "filter-type-state-entity":"Encja ze stanu pulpitu nawigacyjnego", + "filter-type-state-entity-description":"Encja pobrana z parametrów stanu pulpitu nawigacyjnego", + "filter-type-asset-type":"Typ aktywa", + "filter-type-asset-type-description":"Aktywa typu '{{assetTypes}}'", + "filter-type-asset-type-and-name-description":"Aktywa typu '{{assetTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", + "filter-type-device-type":"Typ urządzenia", + "filter-type-device-type-description":"Urządzenia typu '{{deviceTypes}}'", + "filter-type-device-type-and-name-description":"Urządzenia typu '{{deviceTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", + "filter-type-entity-view-type":"Typ widoku encji", + "filter-type-entity-view-type-description":"Widoki encji typu '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description":"Widoki encji typu '{{entityViewTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", + "filter-type-edge-type":"Typ krawędzi", + "filter-type-edge-type-description":"Krawędzie typu '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description":"Krawędzie typu '{{edgeTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", + "filter-type-relations-query":"Zapytanie o relacje", + "filter-type-relations-query-description":"{{entities}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-asset-search-query":"Zapytanie wyszukiwania aktywów", + "filter-type-asset-search-query-description":"Aktywa typu {{assetTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-device-search-query":"Zapytanie wyszukiwania urządzeń", + "filter-type-device-search-query-description":"Urządzenia typu {{deviceTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query":"Zapytanie wyszukiwania widoków encji", + "filter-type-entity-view-search-query-description":"Widoki encji typu {{entityViewTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", + "filter-type-apiUsageState":"Stan użycia API", + "filter-type-edge-search-query":"Zapytanie wyszukiwania krawędzi", + "filter-type-edge-search-query-description":"Krawędzie typu {{edgeTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", + "entity-filter":"Filtr encji", + "resolve-multiple":"Rozwiąż jako wiele encji", + "filter-type":"Typ filtra", + "filter-type-required":"Typ filtra jest wymagany.", + "entity-filter-no-entity-matched":"Nie znaleziono encji pasujących do określonego filtra.", + "no-entity-filter-specified":"Nie określono filtra encji", + "root-state-entity":"Użyj encji stanu pulpitu nawigacyjnego jako korzenia", + "last-level-relation":"Pobierz tylko relacje ostatniego poziomu", + "root-entity":"Encja główna", + "state-entity-parameter-name":"Nazwa parametru encji stanu", + "default-state-entity":"Domyślna encja stanu", + "default-entity-parameter-name":"Domyślnie", + "max-relation-level":"Maksymalny poziom relacji", + "unlimited-level":"Nieograniczony poziom", + "state-entity":"Encja stanu pulpitu nawigacyjnego", + "all-entities":"Wszystkie encje", + "any-relation":"dowolna" + }, + "asset":{ + "asset":"Aktywa", + "assets":"Aktywa", + "management":"Zarządzanie aktywami", + "view-assets":"Wyświetl aktywa", + "add":"Dodaj aktywa", + "asset-type-max-length":"Typ aktywa powinien być krótszy niż 256", + "assign-to-customer":"Przypisz do klienta", + "assign-asset-to-customer":"Przypisz aktywa do klienta", + "assign-asset-to-customer-text":"Proszę wybrać aktywa do przypisania klientowi", + "no-assets-text":"Nie znaleziono aktywów", + "assign-to-customer-text":"Proszę wybrać klienta do przypisania aktywów", + "public":"Publiczne", + "assignedToCustomer":"Przypisane do klienta", + "make-public":"Upublicznij aktywa", + "make-private":"Prywatyzuj aktywa", + "unassign-from-customer":"Usuń przypisanie od klienta", + "delete":"Usuń aktywa", + "asset-public":"Aktywa są publiczne", + "asset-type":"Typ aktywa", + "asset-type-required":"Typ aktywa jest wymagany.", + "select-asset-type":"Wybierz typ aktywa", + "enter-asset-type":"Wprowadź typ profilu aktywa", + "any-asset":"Dowolne aktywa", + "no-asset-types-matching":"Nie znaleziono typów aktywów pasujących do '{{entitySubtype}}'.", + "asset-type-list-empty":"Nie wybrano typów aktywów.", + "asset-types":"Typy aktywów", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "name-max-length":"Nazwa powinna być krótsza niż 256", + "label-max-length":"Etykieta powinna być krótsza niż 256", + "description":"Opis", + "type":"Typ", + "type-required":"Typ jest wymagany.", + "details":"Szczegóły", + "events":"Wydarzenia", + "add-asset-text":"Dodaj nowe aktywa", + "asset-details":"Szczegóły aktywa", + "assign-assets":"Przypisz aktywa", + "assign-assets-text":"Przypisz { count, plural, =1 {1 aktywa} other {# aktywów} } do klienta", + "assign-asset-to-edge-title":"Przypisz aktywa do krawędzi", + "assign-asset-to-edge-text":"Proszę wybrać aktywa do przypisania do krawędzi", + "delete-assets":"Usuń aktywa", + "unassign-assets":"Usuń przypisanie aktywów", + "unassign-assets-action-title":"Usuń przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} } od klienta", + "assign-new-asset":"Przypisz nowe aktywa", + "delete-asset-title":"Czy na pewno chcesz usunąć aktywa '{{assetName}}'?", + "delete-asset-text":"Ostrożnie, po potwierdzeniu aktywa oraz wszystkie powiązane dane staną się nieodwracalne.", + "delete-assets-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 aktywa} other {# aktywów} }?", + "delete-assets-action-title":"Usuń { count, plural, =1 {1 aktywa} other {# aktywów} }", + "delete-assets-text":"Ostrożnie, po potwierdzeniu wszystkie wybrane aktywa oraz wszystkie powiązane dane staną się nieodwracalne.", + "make-public-asset-title":"Czy na pewno chcesz uczynić aktywa '{{assetName}}' publicznymi?", + "make-public-asset-text":"Po potwierdzeniu aktywa oraz wszystkie jego dane staną się publicznie dostępne.", + "make-private-asset-title":"Czy na pewno chcesz uczynić aktywa '{{assetName}}' prywatnymi?", + "make-private-asset-text":"Po potwierdzeniu aktywa oraz wszystkie jego dane staną się prywatne i niedostępne dla innych.", + "unassign-asset-title":"Czy na pewno chcesz usunąć przypisanie aktywów '{{assetName}}'?", + "unassign-asset-text":"Po potwierdzeniu aktywa zostaną usunięte z przypisania i nie będą dostępne dla klienta.", + "unassign-asset":"Usuń przypisanie aktywów", + "unassign-assets-title":"Czy na pewno chcesz usunąć przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} }?", + "unassign-assets-text":"Po potwierdzeniu wszystkie wybrane aktywa zostaną usunięte z przypisania i nie będą dostępne dla klienta.", + "unassign-assets-from-edge":"Usuń przypisanie aktywów od krawędzi", + "copyId":"Kopiuj ID aktywów", + "idCopiedMessage":"ID aktywów zostało skopiowane do schowka", + "select-asset":"Wybierz aktywa", + "no-assets-matching":"Nie znaleziono aktywów pasujących do '{{entity}}'.", + "asset-required":"Aktywa są wymagane", + "name-starts-with":"Wyrażenie nazwy aktywów", + "help-text":"Użyj '%', zgodnie z potrzebą: '%nazwa_zawiera_aktywa%', '%nazwa_kończy_się_aktywami', 'nazwa_rozpoczyna_się_aktywami'.", + "import":"Importuj aktywa", + "asset-file":"Plik aktywów", + "label":"Etykieta", + "search":"Wyszukaj aktywa", + "assign-asset-to-edge":"Przypisz aktywa do krawędzi", + "unassign-asset-from-edge":"Usuń przypisanie aktywów od krawędzi", + "unassign-asset-from-edge-title":"Czy na pewno chcesz usunąć przypisanie aktywów '{{assetName}}' od krawędzi?", + "unassign-asset-from-edge-text":"Po potwierdzeniu aktywa zostaną usunięte z przypisania od krawędzi i nie będą dostępne dla tej krawędzi.", + "unassign-assets-from-edge-title":"Czy na pewno chcesz usunąć przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} } od krawędzi?", + "unassign-assets-from-edge-text":"Po potwierdzeniu wszystkie wybrane aktywa zostaną usunięte z przypisania od krawędzi i nie będą dostępne dla tej krawędzi.", + "selected-assets":"{ count, plural, =1 {1 aktywo} other {# aktywów} } wybrane" + }, + "attribute":{ + "attributes":"Atrybuty", + "latest-telemetry":"Najnowsza telemetria", + "no-latest-telemetry":"Brak najnowszej telemetrii", + "attributes-scope":"Zakres atrybutów encji", + "scope-telemetry":"Telemetria", + "scope-latest-telemetry":"Najnowsza telemetria", + "scope-client":"Atrybuty klienta", + "scope-server":"Atrybuty serwera", + "scope-shared":"Atrybuty współdzielone", + "add":"Dodaj atrybut", + "key":"Klucz", + "key-max-length":"Klucz powinien być krótszy niż 256", + "last-update-time":"Czas ostatniej aktualizacji", + "key-required":"Klucz atrybutu jest wymagany.", + "value":"Wartość", + "value-required":"Wartość atrybutu jest wymagana.", + "telemetry-key-required":"Klucz telemetrii jest wymagany", + "telemetry-value-required":"Wartość telemetrii jest wymagana", + "delete-attributes-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 atrybut} other {# atrybutów} }?", + "delete-attributes-text":"Ostrożnie, po potwierdzeniu wszystkie wybrane atrybuty zostaną usunięte.", + "delete-attributes":"Usuń atrybuty", + "enter-attribute-value":"Wprowadź wartość atrybutu", + "show-on-widget":"Pokaż na widżecie", + "widget-mode":"Tryb widżetu", + "next-widget":"Następny widżet", + "prev-widget":"Poprzedni widżet", + "add-to-dashboard":"Dodaj do pulpitu nawigacyjnego", + "add-widget-to-dashboard":"Dodaj widżet do pulpitu nawigacyjnego", + "selected-attributes":"{ count, plural, =1 {1 atrybut} other {# atrybutów} } wybrano", + "selected-telemetry":"{ count, plural, =1 {1 jednostka telemetrii} other {# jednostek telemetrii} } wybrano", + "no-attributes-text":"Nie znaleziono atrybutów", + "no-telemetry-text":"Nie znaleziono telemetrii", + "copy-key":"Kopiuj klucz", + "add-telemetry":"Dodaj telemetrię", + "copy-value":"Kopiuj wartość", + "delete-timeseries":{ + "start-time":"Czas rozpoczęcia", + "ends-on":"Kończy się", + "strategy":"Strategia", + "delete-strategy":"Strategia usuwania", + "all-data":"Usuń wszystkie dane", + "all-data-except-latest-value":"Usuń wszystkie dane oprócz najnowszej wartości", + "latest-value":"Usuń najnowszą wartość", + "all-data-for-time-period":"Usuń wszystkie dane za okres czasu", + "rewrite-latest-value":"Zastąp najnowszą wartość" + } + }, + "api-usage":{ + "api-features":"Funkcje API", + "api-usage":"Użycie API", + "alarm":"Alarm", + "alarms-created":"Utworzone alarmy", + "alarms-created-daily-activity":"Dzienna aktywność utworzonych alarmów", + "alarms-created-hourly-activity":"Godzinna aktywność utworzonych alarmów", + "alarms-created-monthly-activity":"Miesięczna aktywność utworzonych alarmów", + "data-points":"Punkty danych", + "data-points-storage-days":"Dni przechowywania punktów danych", + "device-api":"API urządzenia", + "email":"Email", + "email-messages":"Wiadomości e-mail", + "email-messages-daily-activity":"Dzienna aktywność wiadomości e-mail", + "email-messages-monthly-activity":"Miesięczna aktywność wiadomości e-mail", + "exceptions":"Wyjątki", + "executions":"Wykonania", + "scripts":"Skrypty", + "scripts-hourly-activity":"Godzinna aktywność skryptów", + "scripts-daily-activity":"Dzienna aktywność skryptów", + "scripts-monthly-activity":"Miesięczna aktywność skryptów", + "javascript":"JavaScript", + "javascript-executions":"Wykonania JavaScript", + "tbel":"TBEL", + "tbel-executions":"Wykonania TBEL", + "latest-error":"Ostatni błąd", + "messages":"Wiadomości", + "notifications":"Powiadomienia", + "notifications-email-sms":"Powiadomienia (Email/SMS)", + "notifications-hourly-activity":"Godzinna aktywność powiadomień", + "permanent-failures":"Stałe awarie ${entityName}", + "permanent-timeouts":"Stałe przekroczenia czasu ${entityName}", + "processing-failures":"Błędy przetwarzania ${entityName}", + "processing-failures-and-timeouts":"Błędy przetwarzania i przekroczenia czasu", + "processing-timeouts":"Przekroczenia czasu przetwarzania ${entityName}", + "queue-stats":"Statystyki kolejki", + "rule-chain":"Łańcuch reguł", + "rule-engine":"Silnik reguł", + "rule-engine-daily-activity":"Dzienna aktywność silnika reguł", + "rule-engine-executions":"Wykonania silnika reguł", + "rule-engine-hourly-activity":"Godzinna aktywność silnika reguł", + "rule-engine-monthly-activity":"Miesięczna aktywność silnika reguł", + "rule-engine-statistics":"Statystyki silnika reguł", + "rule-node":"Węzeł reguł", + "sms":"SMS", + "sms-messages":"Wiadomości SMS", + "sms-messages-daily-activity":"Dzienna aktywność wiadomości SMS", + "sms-messages-monthly-activity":"Miesięczna aktywność wiadomości SMS", + "successful":"Udane ${entityName}", + "telemetry":"Telemetria", + "telemetry-persistence":"Przechowywanie telemetrii", + "telemetry-persistence-daily-activity":"Dzienna aktywność przechowywania telemetrii", + "telemetry-persistence-hourly-activity":"Godzinna aktywność przechowywania telemetrii", + "telemetry-persistence-monthly-activity":"Miesięczna aktywność przechowywania telemetrii", + "transport":"Transport", + "transport-daily-activity":"Dzienna aktywność transportu", + "transport-data-points":"Punkty danych transportu", + "transport-hourly-activity":"Godzinna aktywność transportu", + "transport-messages":"Wiadomości transportu", + "transport-monthly-activity":"Miesięczna aktywność transportu", + "view-details":"Wyświetl szczegóły", + "view-statistics":"Wyświetl statystyki" + }, + "api-limit":{ + "cassandra-queries":"Zapytania do Cassandry", + "entity-version-creation":"Tworzenie wersji encji", + "entity-version-load":"Ładowanie wersji encji", + "notification-requests":"Żądania powiadomień", + "notification-requests-per-rule":"Żądania powiadomień na regułę", + "rest-api-requests":"Żądania REST API", + "rest-api-requests-per-customer":"Żądania REST API na klienta", + "transport-messages":"Wiadomości transportowe", + "transport-messages-per-device":"Wiadomości transportowe na urządzenie", + "ws-updates-per-session":"Aktualizacje WS na sesję" + }, + "audit-log":{ + "audit":"Audyt", + "audit-logs":"Dzienniki audytu", + "timestamp":"Znacznik czasu", + "entity-type":"Typ encji", + "entity-name":"Nazwa encji", + "user":"Użytkownik", + "type":"Typ", + "status":"Status", + "details":"Szczegóły", + "type-added":"Dodano", + "type-deleted":"Usunięto", + "type-updated":"Zaktualizowano", + "type-attributes-updated":"Zaktualizowano atrybuty", + "type-attributes-deleted":"Usunięto atrybuty", + "type-rpc-call":"Wywołanie RPC", + "type-credentials-updated":"Zaktualizowano dane uwierzytelniające", + "type-assigned-to-customer":"Przypisano do klienta", + "type-unassigned-from-customer":"Usunięto przypisanie od klienta", + "type-assigned-to-edge":"Przypisano do krawędzi", + "type-unassigned-from-edge":"Usunięto przypisanie od krawędzi", + "type-activated":"Aktywowano", + "type-suspended":"Zawieszono", + "type-credentials-read":"Odczytano dane uwierzytelniające", + "type-attributes-read":"Odczytano atrybuty", + "type-relation-add-or-update":"Zaktualizowano relację", + "type-relation-delete":"Usunięto relację", + "type-relations-delete":"Usunięto wszystkie relacje", + "type-alarm-ack":"Potwierdzono", + "type-alarm-clear":"Wyczyszczono", + "type-alarm-assign":"Przypisano", + "type-alarm-unassign":"Usunięto przypisanie", + "type-added-comment":"Dodano komentarz", + "type-updated-comment":"Zaktualizowano komentarz", + "type-deleted-comment":"Usunięto komentarz", + "type-login":"Logowanie", + "type-logout":"Wylogowanie", + "type-lockout":"Blokada", + "status-success":"Sukces", + "status-failure":"Niepowodzenie", + "audit-log-details":"Szczegóły dziennika audytu", + "no-audit-logs-prompt":"Nie znaleziono dzienników", + "action-data":"Dane akcji", + "failure-details":"Szczegóły niepowodzenia", + "search":"Szukaj w dziennikach audytu", + "clear-search":"Wyczyść wyszukiwanie", + "type-assigned-from-tenant":"Przypisano od dzierżawcy", + "type-assigned-to-tenant":"Przypisano do dzierżawcy", + "type-provision-success":"Pomyślne przygotowanie urządzenia", + "type-provision-failure":"Niepowodzenie przygotowania urządzenia", + "type-timeseries-updated":"Zaktualizowano telemetrię", + "type-timeseries-deleted":"Usunięto telemetrię", + "type-sms-sent":"Wysłano SMS" + }, + "confirm-on-exit":{ + "message":"Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?", + "html-message":"Masz niezapisane zmiany.
Czy na pewno chcesz opuścić tę stronę?", + "title":"Niezapisane zmiany" + }, + "contact":{ + "country":"Kraj", + "city":"Miasto", + "state":"Stan / Prowincja", + "postal-code":"Kod pocztowy", + "postal-code-invalid":"Nieprawidłowy format kodu pocztowego.", + "address":"Adres", + "address2":"Adres 2", + "phone":"Telefon", + "email":"Email", + "no-address":"Brak adresu", + "state-max-length":"Długość stanu powinna być mniejsza niż 256", + "phone-max-length":"Numer telefonu powinien być krótszy niż 256", + "city-max-length":"Nazwa miasta powinna być krótsza niż 256" + }, + "common":{ + "username":"Nazwa użytkownika", + "password":"Hasło", + "enter-username":"Wprowadź nazwę użytkownika", + "enter-password":"Wprowadź hasło", + "enter-search":"Wprowadź wyszukiwanie", + "created-time":"Czas utworzenia", + "loading":"Ładowanie...", + "proceed":"Kontynuuj", + "open-details-page":"Otwórz stronę szczegółów", + "not-found":"Nie znaleziono", + "documentation":"Dokumentacja" + }, + "content-type":{ + "json":"Json", + "text":"Tekst", + "binary":"Binarny (Base64)" + }, + "customer":{ + "customer":"Klient", + "customers":"Klienci", + "management":"Zarządzanie klientami", + "dashboard":"Pulpit nawigacyjny klienta", + "dashboards":"Pulpity nawigacyjne klienta", + "devices":"Urządzenia klienta", + "entity-views":"Widoki encji klienta", + "assets":"Aktywa klienta", + "public-dashboards":"Publiczne pulpity nawigacyjne", + "public-devices":"Publiczne urządzenia", + "public-assets":"Publiczne aktywa", + "public-edges":"Publiczne krawędzie", + "public-entity-views":"Publiczne widoki encji", + "add":"Dodaj klienta", + "delete":"Usuń klienta", + "manage-customer-users":"Zarządzaj użytkownikami klienta", + "manage-customer-devices":"Zarządzaj urządzeniami klienta", + "manage-customer-dashboards":"Zarządzaj pulpitami nawigacyjnymi klienta", + "manage-public-devices":"Zarządzaj publicznymi urządzeniami", + "manage-public-dashboards":"Zarządzaj publicznymi pulpitami nawigacyjnymi", + "manage-customer-assets":"Zarządzaj aktywami klienta", + "manage-public-assets":"Zarządzaj publicznymi aktywami", + "manage-customer-edges":"Zarządzaj krawędziami klienta", + "manage-public-edges":"Zarządzaj publicznymi krawędziami", + "add-customer-text":"Dodaj nowego klienta", + "no-customers-text":"Nie znaleziono klientów", + "customer-details":"Szczegóły klienta", + "delete-customer-title":"Czy na pewno chcesz usunąć klienta '{{customerTitle}}'?", + "delete-customer-text":"Uważaj, po potwierdzeniu klient i wszystkie powiązane dane staną się nieodwracalne.", + "delete-customers-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 klienta} other {# klientów} }?", + "delete-customers-action-title":"Usuń { count, plural, =1 {1 klienta} other {# klientów} }", + "delete-customers-text":"Uważaj, po potwierdzeniu wszyscy wybrani klienci oraz wszystkie powiązane dane staną się nieodwracalne.", + "manage-users":"Zarządzaj użytkownikami", + "manage-assets":"Zarządzaj aktywami", + "manage-devices":"Zarządzaj urządzeniami", + "manage-dashboards":"Zarządzaj pulpitami nawigacyjnymi", + "title":"Tytuł", + "title-required":"Tytuł jest wymagany.", + "title-max-length":"Tytuł powinien być krótszy niż 256", + "description":"Opis", + "details":"Szczegóły", + "events":"Wydarzenia", + "copyId":"Kopiuj ID klienta", + "idCopiedMessage":"ID klienta zostało skopiowane do schowka", + "select-customer":"Wybierz klienta", + "no-customers-matching":"Nie znaleziono klientów pasujących do '{{entity}}'.", + "customer-required":"Klient jest wymagany", + "select-default-customer":"Wybierz domyślnego klienta", + "default-customer":"Domyślny klient", + "default-customer-required":"Domyślny klient jest wymagany do debugowania pulpitu nawigacyjnego na poziomie dzierżawcy", + "search":"Szukaj klientów", + "selected-customers":"{ count, plural, =1 {1 klient} other {# klientów} } wybrano", + "edges":"Instancje krawędzi klienta", + "manage-edges":"Zarządzaj krawędziami" + }, + "date":{ + "last-update-n-ago":"Ostatnia aktualizacja N temu", + "last-update-n-ago-text":"Ostatnia aktualizacja {{ agoText }}", + "custom-date":"Niestandardowa data", + "format":"Format", + "preview":"Podgląd" + }, + "datetime":{ + "date-from":"Data od", + "time-from":"Czas od", + "date-to":"Data do", + "time-to":"Czas do" + }, + "dashboard":{ + "dashboard":"Panel", + "dashboards":"Panele", + "management":"Zarządzanie panelami", + "view-dashboards":"Przeglądaj panele", + "add":"Dodaj panel", + "assign-dashboard-to-customer":"Przypisz panel(y) do klienta", + "assign-dashboard-to-customer-text":"Wybierz panele do przypisania do klienta", + "assign-dashboard-to-edge-title":"Przypisz panel(y) do urządzenia", + "assign-to-customer-text":"Wybierz klienta, do którego przypisane będą panele", + "assign-to-customer":"Przypisz klientowi", + "unassign-from-customer":"Odłącz od klienta", + "make-public":"Udostępnij panel publicznie", + "make-private":"Ustaw panel jako prywatny", + "manage-assigned-customers":"Zarządzaj przypisanymi klientami", + "assigned-customers":"Przypisani klienci", + "assign-to-customers":"Przypisz panele klientom", + "assign-to-customers-text":"Wybierz klientów, do których przypisane będą panele", + "unassign-from-customers":"Odłącz panele od klientów", + "unassign-from-customers-text":"Wybierz klientów, od których odłączone zostaną panele", + "no-dashboards-text":"Nie znaleziono paneli", + "no-widgets":"Brak skonfigurowanych widżetów", + "add-widget":"Dodaj nowy widżet", + "add-widget-button-text":"Dodaj widżet", + "title":"Tytuł", + "image":"Obraz panelu", + "mobile-app-settings":"Ustawienia aplikacji mobilnej", + "mobile-order":"Kolejność panelu w aplikacji mobilnej", + "mobile-hide":"Ukryj panel w aplikacji mobilnej", + "update-image":"Aktualizuj obraz panelu", + "take-screenshot":"Zrób zrzut ekranu", + "select-widget-title":"Wybierz widżet", + "select-widget-value":"{{title}}: wybierz widżet", + "select-widget-subtitle":"Lista dostępnych typów widżetów", + "delete":"Usuń panel", + "title-required":"Tytuł jest wymagany.", + "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", + "description":"Opis", + "details":"Szczegóły", + "dashboard-details":"Szczegóły panelu", + "add-dashboard-text":"Dodaj nowy panel", + "assign-dashboards":"Przypisz panele", + "assign-new-dashboard":"Przypisz nowy panel", + "assign-dashboards-text":"Przypisz { count, plural, =1 {1 panel} other {# panele} } do klientów", + "unassign-dashboards-action-text":"Odłącz { count, plural, =1 {1 panel} other {# panele} } od klientów", + "delete-dashboards":"Usuń panele", + "unassign-dashboards":"Odłącz panele", + "unassign-dashboards-action-title":"Odłącz { count, plural, =1 {1 panel} other {# panele} } od klienta", + "delete-dashboard-title":"Czy na pewno chcesz usunąć panel '{{dashboardTitle}}'?", + "delete-dashboard-text":"Bądź ostrożny, po potwierdzeniu panel i wszystkie związane z nim dane staną się nieodwracalnie utracone.", + "delete-dashboards-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 panel} other {# panele} }?", + "delete-dashboards-action-title":"Usuń { count, plural, =1 {1 panel} other {# panele} }", + "delete-dashboards-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane panele zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", + "unassign-dashboard-title":"Czy na pewno chcesz odłączyć panel '{{dashboardTitle}}'?", + "unassign-dashboard-text":"Po potwierdzeniu panel zostanie odłączony i nie będzie dostępny dla klienta.", + "unassign-dashboard":"Odłącz panel", + "unassign-dashboards-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 panel} other {# panele} }?", + "unassign-dashboards-text":"Po potwierdzeniu wszystkie wybrane panele zostaną odłączone i nie będą dostępne dla klienta.", + "public-dashboard-title":"Panel jest teraz publiczny", + "public-dashboard-text":"Twój panel {{dashboardTitle}} jest teraz publiczny i dostępny za pomocą następującego publicznego linku:", + "public-dashboard-notice":"Uwaga: Nie zapomnij udostępnić powiązanych urządzeń publicznie, aby uzyskać do nich dostęp.", + "make-private-dashboard-title":"Czy na pewno chcesz uczynić panel '{{dashboardTitle}}' prywatnym?", + "make-private-dashboard-text":"Po potwierdzeniu panel zostanie uczyniony prywatnym i nie będzie dostępny dla innych.", + "make-private-dashboard":"Ustaw panel jako prywatny", + "socialshare-text":"'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title":"'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard":"Wybierz panel", + "no-dashboards-matching":"Nie znaleziono paneli pasujących do '{{entity}}'", + "dashboard-required":"Panel jest wymagany.", + "select-existing":"Wybierz istniejący panel", + "create-new":"Utwórz nowy panel", + "new-dashboard-title":"Tytuł nowego panelu", + "open-dashboard":"Otwórz panel", + "set-background":"Ustaw tło", + "background-color":"Kolor tła", + "background-image":"Obraz tła", + "background-size-mode":"Tryb rozmiaru tła", + "no-image":"Brak wybranego obrazu", + "empty-image":"Brak obrazu", + "drop-image":"Upuść obraz lub kliknij, aby wybrać plik do przesłania.", + "maximum-upload-file-size":"Maksymalny rozmiar pliku do przesłania: {{ size }}", + "cannot-upload-file":"Nie można przesłać pliku", + "settings":"Ustawienia", + "layout-settings":"Ustawienia układu", + "columns-count":"Liczba kolumn", + "columns-count-required":"Liczba kolumn jest wymagana.", + "min-columns-count-message":"Dozwolona jest tylko minimalna liczba kolumn wynosząca 10.", + "max-columns-count-message":"Dozwolona jest tylko maksymalna liczba kolumn wynosząca 1000.", + "widgets-margins":"Margines między widżetami", + "margin-required":"Wartość marginesu jest wymagana.", + "min-margin-message":"Dozwolona jest tylko wartość minimalna marginesu wynosząca 0.", + "max-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu wynosząca 50.", + "horizontal-margin":"Margines poziomy", + "horizontal-margin-required":"Wartość marginesu poziomego jest wymagana.", + "min-horizontal-margin-message":"Dozwolona jest tylko wartość minimalna marginesu poziomego wynosząca 0.", + "max-horizontal-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu poziomego wynosząca 50.", + "vertical-margin":"Margines pionowy", + "vertical-margin-required":"Wartość marginesu pionowego jest wymagana.", + "min-vertical-margin-message":"Dozwolona jest tylko wartość minimalna marginesu pionowego wynosząca 0.", + "max-vertical-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu pionowego wynosząca 50.", + "apply-outer-margin":"Zastosuj margines do boków układu", + "autofill-height":"Automatycznie wypełnij wysokość układu", + "mobile-layout":"Ustawienia układu na urządzenia mobilne", + "mobile-row-height":"Wysokość rzędu na urządzenia mobilne, px", + "mobile-row-height-required":"Wartość wysokości rzędu na urządzenia mobilne jest wymagana.", + "min-mobile-row-height-message":"Dozwolona jest tylko minimalna wartość wysokości rzędu na urządzenia mobilne wynosząca 5 pikseli.", + "max-mobile-row-height-message":"Dozwolona jest tylko maksymalna wartość wysokości rzędu na urządzenia mobilne wynosząca 200 pikseli.", + "title-settings":"Ustawienia tytułu", + "display-title":"Wyświetl tytuł panelu", + "title-color":"Kolor tytułu", + "toolbar-settings":"Ustawienia paska narzędziowego", + "hide-toolbar":"Ukryj pasek narzędziowy", + "toolbar-always-open":"Zawsze otwarty pasek narzędziowy", + "display-dashboards-selection":"Wyświetl wybór paneli", + "display-entities-selection":"Wyświetl wybór jednostek", + "display-filters":"Wyświetl filtry", + "display-dashboard-timewindow":"Wyświetl okno czasowe panelu", + "display-dashboard-export":"Wyświetl eksport panelu", + "display-update-dashboard-image":"Wyświetl aktualizację obrazu panelu", + "dashboard-logo-settings":"Ustawienia logo panelu", + "display-dashboard-logo":"Wyświetl logo w trybie pełnoekranowym panelu", + "dashboard-logo-image":"Obraz logo panelu", + "advanced-settings":"Zaawansowane ustawienia", + "dashboard-css":"CSS panelu", + "import":"Importuj panel", + "export":"Eksportuj panel", + "export-failed-error":"Nie można wyeksportować panelu: {{error}}", + "create-new-dashboard":"Utwórz nowy panel", + "dashboard-file":"Plik panelu", + "invalid-dashboard-file-error":"Nie można zaimportować panelu: Nieprawidłowa struktura danych panelu.", + "dashboard-import-missing-aliases-title":"Skonfiguruj aliasy używane przez importowany panel", + "create-new-widget":"Utwórz nowy widżet", + "import-widget":"Importuj widżet", + "widget-file":"Plik widżetu", + "invalid-widget-file-error":"Nie można zaimportować widżetu: Nieprawidłowa struktura danych widżetu.", + "widget-import-missing-aliases-title":"Skonfiguruj aliasy używane przez importowany widżet", + "open-toolbar":"Otwórz pasek narzędziowy panelu", + "close-toolbar":"Zamknij pasek narzędziowy panelu", + "configuration-error":"Błąd konfiguracji", + "alias-resolution-error-title":"Błąd konfiguracji aliasów panelu", + "invalid-aliases-config":"Nie można znaleźć żadnych urządzeń pasujących do niektórych filtrów aliasów.
Skontaktuj się z administratorem w celu rozwiązania tego problemu.", + "select-devices":"Wybierz urządzenia", + "assignedToCustomer":"Przypisane do klienta", + "assignedToCustomers":"Przypisane do klientów", + "public":"Publiczne", + "copyId":"Skopiuj identyfikator panelu", + "idCopiedMessage":"Identyfikator panelu został skopiowany do schowka", + "public-link":"Publiczny link", + "copy-public-link":"Skopiuj publiczny link", + "public-link-copied-message":"Publiczny link do panelu został skopiowany do schowka", + "manage-states":"Zarządzaj stanami panelu", + "states":"Stany panelu", + "states-short":"Stany", + "search-states":"Szukaj stanów panelu", + "selected-states":"{ count, plural, =1 {1 stan panelu} other {# stany panelu} } wybranych", + "edit-state":"Edytuj stan panelu", + "delete-state":"Usuń stan panelu", + "add-state":"Dodaj stan panelu", + "no-states-text":"Nie znaleziono stanów", + "state":"Stan panelu", + "state-name":"Nazwa", + "state-name-required":"Wymagana jest nazwa stanu panelu.", + "state-id":"Identyfikator stanu", + "state-id-required":"Wymagane jest Identyfikator stanu panelu.", + "state-id-exists":"Stan panelu o tym samym identyfikatorze już istnieje.", + "is-root-state":"Główny stan", + "delete-state-title":"Usuń stan panelu", + "delete-state-text":"Czy na pewno chcesz usunąć stan panelu o nazwie '{{stateName}}'?", + "show-details":"Pokaż szczegóły", + "hide-details":"Ukryj szczegóły", + "select-state":"Wybierz docelowy stan", + "state-controller":"Kontroler stanu", + "state-controller-default":"statyczny (przestarzały)", + "search":"Szukaj paneli", + "selected-dashboards":"{ count, plural, =1 {1 panel} other {# panele} } wybranych", + "home-dashboard":"Panel główny", + "home-dashboard-hide-toolbar":"Ukryj pasek narzędzi panelu głównego", + "unassign-dashboard-from-edge-text":"Po potwierdzeniu panel zostanie odłączony i nie będzie dostępny dla krawędzi.", + "unassign-dashboards-from-edge-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 panel} other {# panele} }?", + "unassign-dashboards-from-edge-text":"Po potwierdzeniu wszystkie wybrane panele zostaną odłączone i nie będą dostępne dla krawędzi.", + "assign-dashboard-to-edge":"Przypisz panele do krawędzi", + "assign-dashboard-to-edge-text":"Proszę wybrać panele do przypisania do krawędzi", + "non-existent-dashboard-state-error":"Stan panelu o identyfikatorze \"{{ stateId }}\" nie istnieje", + "edit-mode":"Tryb edycji", + "duplicate-state-action":"Zduplikuj stan" + }, + "datakey":{ + "settings":"Ustawienia", + "general":"Ogólne", + "advanced":"Zaawansowane", + "key":"Klucz", + "label":"Etykieta", + "color":"Kolor", + "units":"Symbol specjalny do wyświetlenia obok wartości", + "decimals":"Liczba cyfr po przecinku", + "data-generation-func":"Funkcja generacji danych", + "use-data-post-processing-func":"Użyj funkcji przetwarzania danych po generacji", + "configuration":"Konfiguracja klucza danych", + "timeseries":"Szereg czasowy", + "attributes":"Atrybuty", + "entity-field":"Pole encji", + "alarm":"Pola alarmowe", + "timeseries-required":"Wymagane są serie czasowe encji.", + "timeseries-or-attributes-required":"Wymagane są serie czasowe/cechy encji.", + "alarm-fields-timeseries-or-attributes-required":"Wymagane są pola alarmowe lub serie czasowe/cechy encji.", + "maximum-timeseries-or-attributes":"Maksymalnie { count, plural, =1 {1 seria czasowa/cecha jest dozwolona.} other {# serie czasowe/cechy są dozwolone} }", + "alarm-fields-required":"Wymagane są pola alarmowe.", + "function-types":"Typy funkcji", + "function-type":"Typ funkcji", + "function-types-required":"Wymagane są typy funkcji.", + "data-keys":"Klucze danych", + "data-key":"Klucz danych", + "data-keys-required":"Wymagane są klucze danych.", + "data-key-required":"Wymagany jest klucz danych.", + "alarm-keys":"Klucze danych alarmowych", + "alarm-key":"Klucz danych alarmowych", + "alarm-key-functions":"Funkcje klucza alarmowego", + "alarm-key-function":"Funkcja klucza alarmowego", + "latest-keys":"Ostatnie klucze danych", + "latest-key":"Ostatni klucz danych", + "latest-key-functions":"Funkcje ostatniego klucza", + "latest-key-function":"Funkcja ostatniego klucza", + "timeseries-keys":"Klucze danych szeregów czasowych", + "timeseries-key":"Klucz danych szeregów czasowych", + "timeseries-key-functions":"Funkcje klucza szeregów czasowych", + "timeseries-key-function":"Funkcja klucza szeregów czasowych", + "maximum-function-types":"Maksymalnie { count, plural, =1 {1 typ funkcji jest dozwolony.} other {# typy funkcji są dozwolone} }", + "time-description":"znacznik czasu bieżącej wartości;", + "value-description":"bieżąca wartość;", + "prev-value-description":"wynik poprzedniego wywołania funkcji;", + "time-prev-description":"znacznik czasu poprzedniej wartości;", + "prev-orig-value-description":"oryginalna poprzednia wartość;", + "aggregation":"Agregacja", + "aggregation-type-hint-common":"Ze względów wydajnościowych obliczenia wartości agregowanej są dostępne tylko dla stałych interwałów czasowych, takich jak \"bieżący dzień\", \"bieżący miesiąc\", itp., i nie są dostępne dla interwałów przesuwnych, takich jak 'ostatnie 30 minut' lub 'ostatnie 24 godziny'.", + "aggregation-type-none-hint":"Weź ostatnią wartość.", + "aggregation-type-min-hint":"Znajdź minimalną wartość między punktami danych w wybranym oknie czasowym.", + "aggregation-type-max-hint":"Znajdź maksymalną wartość między punktami danych w wybranym oknie czasowym.", + "aggregation-type-avg-hint":"Oblicz średnią wartość między punktami danych w wybranym oknie czasowym.", + "aggregation-type-sum-hint":"Dodaj wszystkie wartości punktów danych w wybranym oknie czasowym.", + "aggregation-type-count-hint":"Całkowita liczba punktów danych w wybranym oknie czasowym.", + "delta-calculation":"Obliczenia delta", + "enable-delta-calculation":"Włącz obliczenia delta", + "enable-delta-calculation-hint":"Po włączeniu, wartość klucza danych jest obliczana na podstawie wartości agregowanych w wybranym oknie czasowym i okresie porównawczym. Ze względów wydajnościowych obliczenia delta są dostępne tylko dla okien czasowych historycznych, a nie dla wartości czasu rzeczywistego. Na przykład możesz obliczyć różnicę między zużyciem energii wczoraj a zużyciem energii przedwczoraj.", + "delta-calculation-result":"Wynik obliczeń delta", + "delta-calculation-result-previous-value":"Poprzednia wartość", + "delta-calculation-result-delta-absolute":"Delta (bezwzględna)", + "delta-calculation-result-delta-percent":"Delta (procentowa)", + "source":"Źródło", + "latest":"Najnowsze", + "latest-value":"Najnowsza wartość", + "delta":"delta", + "percent":"procent", + "absolute":"bezwzględna" + }, + "datasource":{ + "type":"Typ źródła danych", + "name":"Nazwa", + "label":"Etykieta", + "add-datasource-prompt":"Proszę dodać źródło danych" + }, + "details":{ + "details":"Szczegóły", + "edit-mode":"Tryb edycji", + "edit-json":"Edytuj JSON", + "toggle-edit-mode":"Przełącz tryb edycji" + }, + "device":{ + "device":"Urządzenie", + "device-required":"Urządzenie jest wymagane.", + "devices":"Urządzenia", + "management":"Zarządzanie urządzeniem", + "view-devices":"Zobacz urządzenia", + "device-alias":"Alias urządzenia", + "device-type-max-length":"Typ urządzenia powinien mieć mniej niż 256 znaków", + "aliases":"Aliasy urządzeń", + "no-alias-matching":"'{{alias}}' nie znaleziono.", + "no-aliases-found":"Nie znaleziono aliasów.", + "no-key-matching":"'{{key}}' nie znaleziono.", + "no-keys-found":"Nie znaleziono kluczy.", + "create-new-alias":"Utwórz nowy!", + "create-new-key":"Utwórz nowy!", + "duplicate-alias-error":"Znaleziono duplikat aliasu '{{alias}}'.
Aliasy urządzeń muszą być unikalne w ramach panelu.", + "configure-alias":"Konfiguruj alias '{{alias}}'", + "no-devices-matching":"Nie znaleziono urządzeń pasujących do '{{entity}}'.", + "alias":"Alias", + "alias-required":"Alias urządzenia jest wymagany.", + "remove-alias":"Usuń alias urządzenia", + "add-alias":"Dodaj alias urządzenia", + "name-starts-with":"Wyrażenie nazwy urządzenia", + "help-text":"Użyj '%' według potrzeb: '%device_name_contains%', '%device_name_ends%', 'device_starts_with'.", + "device-list":"Lista urządzeń", + "use-device-name-filter":"Użyj filtra", + "device-list-empty":"Brak wybranych urządzeń.", + "device-name-filter-required":"Filtr nazwy urządzenia jest wymagany.", + "device-name-filter-no-device-matched":"Nie znaleziono urządzeń rozpoczynających się od '{{device}}'.", + "add":"Dodaj urządzenie", + "assign-to-customer":"Przypisz do klienta", + "assign-device-to-customer":"Przypisz urządzenia do klienta", + "assign-device-to-customer-text":"Proszę wybrać urządzenia do przypisania do klienta", + "assign-device-to-edge-title":"Przypisz urządzenia do krawędzi", + "assign-device-to-edge-text":"Proszę wybrać urządzenia do przypisania do krawędzi", + "make-public":"Udostępnij publicznie", + "make-private":"Udostępnij prywatnie", + "no-devices-text":"Nie znaleziono urządzeń", + "assign-to-customer-text":"Proszę wybrać klienta, do którego przypisane będą urządzenia", + "device-details":"Szczegóły urządzenia", + "add-device-text":"Dodaj nowe urządzenie", + "credentials":"Poświadczenia", + "manage-credentials":"Zarządzaj poświadczeniami", + "delete":"Usuń urządzenie", + "assign-devices":"Przypisz urządzenia", + "assign-devices-text":"Przypisz { count, plural, =1 {1 urządzenie} other {# urządzeń} } do klienta", + "delete-devices":"Usuń urządzenia", + "unassign-from-customer":"Odłącz od klienta", + "unassign-devices":"Odłącz urządzenia", + "unassign-devices-action-title":"Odłącz { count, plural, =1 {1 urządzenie} other {# urządzeń} } od klienta", + "unassign-device-from-edge-title":"Czy na pewno chcesz odłączyć urządzenie '{{deviceName}}'?", + "unassign-device-from-edge-text":"Po potwierdzeniu urządzenie zostanie odłączone i nie będzie dostępne przez krawędź.", + "unassign-devices-from-edge":"Odłącz urządzenia od krawędzi", + "assign-new-device":"Przypisz nowe urządzenie", + "make-public-device-title":"Czy na pewno chcesz udostępnić publicznie urządzenie '{{deviceName}}'?", + "make-public-device-text":"Po potwierdzeniu urządzenie i wszystkie jego dane będą dostępne publicznie dla innych.", + "make-private-device-title":"Czy na pewno chcesz udostępnić prywatnie urządzenie '{{deviceName}}'?", + "make-private-device-text":"Po potwierdzeniu urządzenie i wszystkie jego dane będą dostępne prywatnie i nie będą dostępne dla innych.", + "view-credentials":"Zobacz poświadczenia", + "delete-device-title":"Czy na pewno chcesz usunąć urządzenie '{{deviceName}}'?", + "delete-device-text":"Bądź ostrożny, po potwierdzeniu urządzenie i wszystkie powiązane dane staną się nieodwracalnie utracone.", + "delete-devices-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", + "delete-devices-action-title":"Usuń { count, plural, =1 {1 urządzenie} other {# urządzeń} }", + "delete-devices-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane urządzenia zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalnie utracone.", + "unassign-device-title":"Czy na pewno chcesz odłączyć urządzenie '{{deviceName}}'?", + "unassign-device-text":"Po potwierdzeniu urządzenie zostanie odłączone i nie będzie dostępne przez klienta.", + "unassign-device":"Odłącz urządzenie", + "unassign-devices-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", + "unassign-devices-text":"Po potwierdzeniu wszystkie wybrane urządzenia zostaną odłączone i nie będą dostępne przez klienta.", + "device-credentials":"Poświadczenia urządzenia", + "loading-device-credentials":"Ładowanie poświadczeń urządzenia...", + "credentials-type":"Typ poświadczeń", + "access-token":"Token dostępu", + "access-token-required":"Token dostępu jest wymagany.", + "access-token-invalid":"Długość tokenu dostępu musi wynosić od 1 do 32 znaków.", + "certificate-pem-format":"Certyfikat w formacie PEM", + "certificate-pem-format-required":"Certyfikat jest wymagany.", + "copy-access-token":"Skopiuj token dostępu", + "copy-certificate":"Skopiuj certyfikat", + "copy-client-id":"Skopiuj identyfikator klienta", + "copy-user-name":"Skopiuj nazwę użytkownika", + "copy-password":"Skopiuj hasło", + "generate-client-id":"Generuj identyfikator klienta", + "generate-user-name":"Generuj nazwę użytkownika", + "generate-password":"Generuj hasło", + "generate-access-token":"Generuj token dostępu", + "lwm2m-security-config":{ + "identity":"Identyfikator klienta", + "identity-required":"Identyfikator klienta jest wymagany.", + "identity-tooltip":"Identyfikator PSK to dowolny identyfikator PSK o długości do 128 bajtów, zgodny z opisem w standardzie [RFC7925].\nIdentyfikator PSK MUSI być najpierw przekształcony w ciąg znaków, a następnie zakodowany na bajty przy użyciu UTF-8.", + "client-key":"Klucz klienta", + "client-key-required":"Klucz klienta jest wymagany.", + "client-key-tooltip-prk":"Klucz publiczny RPK lub identyfikator muszą być zgodne ze standardem [RFC7250] i zakodowane w formacie Base64!", + "client-key-tooltip-psk":"Klucz PSK musi być zgodny ze standardem [RFC4279] i formatem HexDec: 32, 64, 128 znaków!", + "endpoint":"Nazwa klienta Endpoint", + "endpoint-required":"Nazwa klienta Endpoint jest wymagana.", + "client-public-key":"Klucz publiczny klienta", + "client-public-key-hint":"Jeśli klucz publiczny klienta jest pusty, używane będzie zaufane certyfikat", + "client-public-key-tooltip":"Klucz publiczny X509 musi być w formacie zakodowanym DER X509v3 i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64!", + "mode":"Tryb konfiguracji zabezpieczeń", + "client-tab":"Konfiguracja zabezpieczeń klienta", + "client-certificate":"Certyfikat klienta", + "bootstrap-tab":"Klient rozruchowy", + "bootstrap-server":"Serwer rozruchowy", + "lwm2m-server":"Serwer LwM2M", + "client-publicKey-or-id":"Klucz publiczny klienta lub Identyfikator", + "client-publicKey-or-id-required":"Klucz publiczny klienta lub Identyfikator są wymagane.", + "client-publicKey-or-id-tooltip-psk":"Identyfikator PSK to dowolny identyfikator PSK o długości do 128 bajtów, zgodny z opisem w standardzie [RFC7925].\nIdentyfikator PSK MUSI być najpierw przekształcony w ciąg znaków, a następnie zakodowany na bajty przy użyciu UTF-8.", + "client-publicKey-or-id-tooltip-rpk":"Klucz publiczny RPK lub identyfikator muszą być zgodne ze standardem [RFC7250] i zakodowane w formacie Base64!", + "client-publicKey-or-id-tooltip-x509":"Klucz publiczny X509 musi być w formacie zakodowanym DER X509v3 i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64!", + "client-secret-key":"Tajny klucz klienta", + "client-secret-key-required":"Tajny klucz klienta jest wymagany.", + "client-secret-key-tooltip-psk":"Klucz PSK musi być zgodny ze standardem [RFC4279] i formatem HexDec: 32, 64, 128 znaków!", + "client-secret-key-tooltip-prk":"Tajny klucz RPK musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]) i zakodowany w formacie Base64!", + "client-secret-key-tooltip-x509":"Tajny klucz X509 musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]) i zakodowany w formacie Base64!" + }, + "client-id":"ID klienta", + "client-id-pattern":"Zawiera nieprawidłowy znak.", + "user-name":"Nazwa użytkownika", + "user-name-required":"Nazwa użytkownika jest wymagana.", + "client-id-or-user-name-necessary":"ID klienta i/lub Nazwa użytkownika są wymagane", + "password":"Hasło", + "secret":"Sekret", + "secret-required":"Sekret jest wymagany.", + "device-type":"Profil urządzenia", + "device-type-required":"Typ urządzenia jest wymagany.", + "select-device-type":"Wybierz typ urządzenia", + "enter-device-type":"Wprowadź profil urządzenia", + "any-device":"Dowolne urządzenie", + "no-device-types-matching":"Nie znaleziono profili urządzeń pasujących do '{{entitySubtype}}'.", + "device-type-list-empty":"Nie wybrano profili urządzeń!", + "device-profile-type-list-empty":"Należy wybrać co najmniej jeden profil urządzenia.", + "device-types":"Profile urządzeń", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "name-max-length":"Nazwa powinna być krótsza niż 256", + "label-max-length":"Etykieta powinna być krótsza niż 256", + "description":"Opis", + "label":"Etykieta", + "events":"Zdarzenia", + "details":"Szczegóły", + "copyId":"Kopiuj ID urządzenia", + "copyAccessToken":"Kopiuj token dostępu", + "copy-mqtt-authentication":"Kopiuj poświadczenia MQTT", + "idCopiedMessage":"ID urządzenia zostało skopiowane do schowka", + "accessTokenCopiedMessage":"Token dostępu do urządzenia został skopiowany do schowka", + "mqtt-authentication-copied-message":"Poświadczenia MQTT urządzenia zostały skopiowane do schowka", + "assignedToCustomer":"Przypisane do klienta", + "unable-delete-device-alias-title":"Nie można usunąć aliasu urządzenia", + "unable-delete-device-alias-text":"Alias urządzenia '{{deviceAlias}}' nie może być usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "is-gateway":"Jest bramą", + "overwrite-activity-time":"Nadpisz czas aktywności podłączonego urządzenia", + "device-filter":"Filtr urządzenia", + "device-filter-title":"Filtr urządzenia", + "filter-title":"Filtr", + "device-state":"Stan urządzenia", + "state":"Stan", + "any":"Dowolny", + "active":"Aktywny", + "inactive":"Nieaktywny", + "public":"Publiczny", + "device-public":"Urządzenie jest publiczne", + "select-device":"Wybierz urządzenie", + "import":"Importuj urządzenie", + "device-file":"Plik urządzenia", + "search":"Wyszukaj urządzenia", + "selected-devices":"{ count, plural, =1 {1 urządzenie} other {# urządzeń} } wybranych", + "device-configuration":"Konfiguracja urządzenia", + "transport-configuration":"Konfiguracja transportu", + "wizard":{ + "device-details":"Szczegóły urządzenia" + }, + "unassign-devices-from-edge-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", + "unassign-devices-from-edge-text":"Po potwierdzeniu wszystkie wybrane urządzenia zostaną odłączone i nie będą dostępne dla krawędzi.", + "time":"Czas", + "connectivity":{ + "check-connectivity":"Sprawdź łączność", + "device-created-check-connectivity":"Urządzenie utworzone. Sprawdźmy łączność!", + "loading-check-connectivity-command":"Ładowanie poleceń sprawdzania łączności...", + "use-following-instructions":"Skorzystaj z poniższych instrukcji, aby wysyłać telemetrię w imieniu urządzenia za pomocą konsoli", + "execute-following-command":"Wykonaj poniższe polecenie", + "install-curl-windows":"Począwszy od systemu Windows 10 b17063, cURL jest dostępny domyślnie", + "install-curl-macos":"Począwszy od Mac OS X 10.2 6C115 (Jaguar), cURL jest dostępny domyślnie", + "install-mqtt-windows":"Użyj instrukcji do pobrania, zainstalowania, skonfigurowania i uruchomienia mosquitto_pub", + "install-coap-client":"Użyj instrukcji do pobrania, zainstalowania, skonfigurowania i uruchomienia klienta coap", + "install-necessary-client-tools":"Zainstaluj niezbędne narzędzia klienta", + "mqtts-x509-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą MQTT z uwierzytelnianiem X509", + "coaps-x509-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą CoAP przez DTLS z uwierzytelnianiem X509", + "snmp-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą SNMP.", + "sparkplug-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą MQTT Sparkplug.", + "lwm2m-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą LWM2M." + } + }, + "asset-profile":{ + "asset-profile":"Profil zasobu", + "asset-profiles":"Profile zasobów", + "all-asset-profiles":"Wszystkie", + "add":"Dodaj profil zasobu", + "edit":"Edytuj profil zasobu", + "asset-profile-details":"Szczegóły profilu zasobu", + "no-asset-profiles-text":"Nie znaleziono profili zasobów", + "search":"Szukaj profili zasobów", + "selected-asset-profiles":"{ count, plural, =1 {1 profil zasobu} other {# profile zasobów} } wybrano", + "no-asset-profiles-matching":"Nie znaleziono profilu zasobu pasującego do '{{entity}}'.", + "asset-profile-required":"Wymagany profil zasobu", + "idCopiedMessage":"Identyfikator profilu zasobu został skopiowany do schowka", + "set-default":"Ustaw jako domyślny profil zasobu", + "delete":"Usuń profil zasobu", + "copyId":"Kopiuj identyfikator profilu zasobu", + "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", + "new-device-profile-name":"Nazwa profilu zasobu", + "new-device-profile-name-required":"Wymagana jest nazwa profilu zasobu.", + "name":"Nazwa", + "name-required":"Wymagana jest nazwa.", + "image":"Zdjęcie profilu zasobu", + "description":"Opis", + "default":"Domyślny", + "default-rule-chain":"Domyślny łańcuch reguł", + "default-edge-rule-chain":"Domyślny łańcuch reguł na krawędzi", + "default-edge-rule-chain-hint":"Używane na krawędzi jako łańcuch reguł do przetwarzania przychodzących danych dla zasobów tego profilu zasobu", + "mobile-dashboard":"Mobilna konsola", + "mobile-dashboard-hint":"Używane przez aplikację mobilną jako konsola szczegółów zasobu", + "select-queue-hint":"Wybierz z listy rozwijanej.", + "delete-asset-profile-title":"Czy na pewno chcesz usunąć profil zasobu '{{assetProfileName}}'?", + "delete-asset-profile-text":"Bądź ostrożny, po potwierdzeniu profil zasobu i wszystkie związane dane staną się nieodwracalnie utracone.", + "delete-asset-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil zasobu} other {# profile zasobów} }?", + "delete-asset-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile zasobów zostaną usunięte, a wszystkie związane dane staną się nieodwracalnie utracone.", + "set-default-asset-profile-title":"Czy na pewno chcesz ustawić profil zasobu '{{assetProfileName}}' jako domyślny?", + "set-default-asset-profile-text":"Po potwierdzeniu profil zasobu zostanie oznaczony jako domyślny i będzie używany dla nowych zasobów bez określonego profilu.", + "no-asset-profiles-found":"Nie znaleziono profili zasobów.", + "create-new-asset-profile":"Utwórz nowy!", + "create-asset-profile":"Utwórz nowy profil zasobu", + "import":"Importuj profil zasobu", + "export":"Eksportuj profil zasobu", + "export-failed-error":"Nie można wyeksportować profilu zasobu: {{error}}", + "asset-profile-file":"Plik profilu zasobu", + "invalid-asset-profile-file-error":"Nie można zaimportować profilu zasobu: Nieprawidłowa struktura danych profilu zasobu." + }, + "device-profile":{ + "device-profile":"Profil urządzenia", + "device-profiles":"Profile urządzeń", + "all-device-profiles":"Wszystkie", + "add":"Dodaj profil urządzenia", + "edit":"Edytuj profil urządzenia", + "device-profile-details":"Szczegóły profilu urządzenia", + "no-device-profiles-text":"Nie znaleziono profili urządzeń", + "search":"Szukaj profili urządzeń", + "selected-device-profiles":"{ count, plural, =1 {1 profil urządzenia} other {# profile urządzeń} } wybrano", + "no-device-profiles-matching":"Nie znaleziono profilu urządzenia pasującego do '{{entity}}'.", + "device-profile-required":"Wymagany profil urządzenia", + "idCopiedMessage":"Identyfikator profilu urządzenia został skopiowany do schowka", + "set-default":"Ustaw jako domyślny profil urządzenia", + "delete":"Usuń profil urządzenia", + "copyId":"Kopiuj identyfikator profilu urządzenia", + "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", + "name":"Nazwa", + "name-required":"Wymagana jest nazwa.", + "type":"Typ profilu", + "type-required":"Wymagany jest typ profilu.", + "type-default":"Domyślny", + "image":"Zdjęcie profilu urządzenia", + "transport-type":"Typ transportu", + "transport-type-required":"Wymagany jest typ transportu.", + "transport-type-default":"Domyślny", + "transport-type-default-hint":"Obsługuje podstawowe transporty MQTT, HTTP i CoAP", + "transport-type-mqtt":"MQTT", + "transport-type-mqtt-hint":"Umożliwia zaawansowane ustawienia transportu MQTT", + "transport-type-coap":"CoAP", + "transport-type-coap-hint":"Umożliwia zaawansowane ustawienia transportu CoAP", + "transport-type-lwm2m":"LWM2M", + "transport-type-lwm2m-hint":"Typ transportu LWM2M", + "transport-type-snmp":"SNMP", + "transport-type-snmp-hint":"Określ konfigurację transportu SNMP", + "transport-type-http":"HTTP", + "description":"Opis", + "default":"Domyślny", + "profile-configuration":"Konfiguracja profilu", + "transport-configuration":"Konfiguracja transportu", + "default-rule-chain":"Domyślny łańcuch reguł", + "default-edge-rule-chain":"Domyślny łańcuch reguł na krawędzi", + "default-edge-rule-chain-hint":"Używane na krawędzi jako łańcuch reguł do przetwarzania przychodzących danych dla urządzeń tego profilu urządzenia", + "mobile-dashboard":"Mobilna konsola", + "mobile-dashboard-hint":"Używane przez aplikację mobilną jako konsola szczegółów urządzenia", + "select-queue-hint":"Wybierz z listy rozwijanej.", + "delete-device-profile-title":"Czy na pewno chcesz usunąć profil urządzenia '{{deviceProfileName}}'?", + "delete-device-profile-text":"Bądź ostrożny, po potwierdzeniu profil urządzenia i wszystkie związane dane, w tym powiązane aktualizacje OTA, staną się nieodwracalnie utracone.", + "delete-device-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil urządzenia} other {# profile urządzeń} }?", + "delete-device-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile urządzeń zostaną usunięte, a wszystkie związane dane, w tym powiązane aktualizacje OTA, staną się nieodwracalnie utracone.", + "set-default-device-profile-title":"Czy na pewno chcesz ustawić profil urządzenia '{{deviceProfileName}}' jako domyślny?", + "set-default-device-profile-text":"Po potwierdzeniu profil urządzenia zostanie oznaczony jako domyślny i będzie używany dla nowych urządzeń bez określonego profilu.", + "no-device-profiles-found":"Nie znaleziono profili urządzeń.", + "create-new-device-profile":"Utwórz nowy!", + "mqtt-device-topic-filters":"Filtr tematów urządzenia MQTT", + "mqtt-device-topic-filters-unique":"Filtry tematów urządzenia MQTT muszą być unikalne.", + "mqtt-device-topic-filters-spark-plug":"Węzeł krawędziowy MQTT Sparkplug B (EoN).", + "mqtt-device-topic-filters-spark-plug-hint":"Zezwól na połączenia z węzłami EoN z ładunkiem i formatem tematu Sparkplug B.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names":"Metryki SparkPlug do przechowywania jako atrybuty.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint":"Nazwy metryk SparkPlug, które będą przechowywane jako atrybuty urządzenia. Wszystkie inne metryki będą przechowywane jako telemetria urządzenia.", + "mqtt-device-payload-type":"Typ ładunka urządzenia MQTT", + "mqtt-device-payload-type-json":"JSON", + "mqtt-device-payload-type-proto":"Protobuf", + "mqtt-enable-compatibility-with-json-payload-format":"Włącz zgodność z innymi formatami ładunków.", + "mqtt-enable-compatibility-with-json-payload-format-hint":"Gdy jest włączona, platforma będzie domyślnie używać formatu ładunku Protobuf. Jeśli parsowanie się nie powiedzie, platforma spróbuje użyć formatu ładunku JSON. Przydatne dla wstecznej kompatybilności podczas aktualizacji oprogramowania układowego. Na przykład początkowa wersja oprogramowania używa Json, podczas gdy nowa wersja używa Protobuf. Podczas procesu aktualizacji oprogramowania dla floty urządzeń konieczne jest jednoczesne obsługiwanie zarówno Protobuf, jak i JSON. Tryb zgodności wprowadza niewielki spadek wydajności, więc zaleca się wyłączenie tego trybu, gdy wszystkie urządzenia zostaną zaktualizowane.", + "mqtt-use-json-format-for-default-downlink-topics":"Użyj formatu Json dla domyślnych tematów zwrotnych", + "mqtt-use-json-format-for-default-downlink-topics-hint":"Gdy jest włączone, platforma będzie używać formatu ładunku Json do przesyłania atrybutów i RPC za pomocą następujących tematów: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Ta ustawienie nie wpływa na subskrypcje atrybutów i rpc wysyłane za pomocą nowych (v2) tematów: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Gdzie $request_id to identyfikator żądania.", + "mqtt-send-ack-on-validation-exception":"Wyślij PUBACK w przypadku niepowodzenia walidacji komunikatu PUBLISH", + "mqtt-send-ack-on-validation-exception-hint":"Domyślnie platforma zakończy sesję MQTT w przypadku niepowodzenia walidacji komunikatu. Gdy jest włączone, platforma wyśle potwierdzenie publikacji zamiast zakończenia sesji.", + "snmp-add-mapping":"Dodaj mapowanie SNMP", + "snmp-mapping-not-configured":"Nie skonfigurowano mapowania OID na szeregi czasowe/telemetrię", + "snmp-timseries-or-attribute-name":"Nazwa szeregów czasowych/cechy do mapowania", + "snmp-timseries-or-attribute-type":"Typ szeregów czasowych/cechy do mapowania", + "snmp-method-pdu-type-get-request":"GetRequest", + "snmp-method-pdu-type-get-next-request":"GetNextRequest", + "snmp-oid":"OID", + "transport-device-payload-type-json":"JSON", + "transport-device-payload-type-proto":"Protobuf", + "mqtt-payload-type-required":"Wymagany jest typ ładunka.", + "coap-device-type":"Typ urządzenia CoAP", + "coap-device-payload-type":"Typ ładunka urządzenia CoAP", + "coap-device-type-required":"Wymagany jest typ urządzenia CoAP.", + "coap-device-type-default":"Domyślny", + "coap-device-type-efento":"Efento NB-IoT", + "support-level-wildcards":"Obsługiwane są pojedyncze [+] i wielopoziomowe [#] znaki wieloznaczne.", + "telemetry-topic-filter":"Filtr tematu telemetrii", + "telemetry-topic-filter-required":"Wymagany jest filtr tematu telemetrii.", + "attributes-topic-filter":"Filtr tematu publikacji atrybutów", + "attributes-subscribe-topic-filter":"Filtr tematu subskrypcji atrybutów", + "attributes-topic-filter-required":"Wymagany jest filtr tematu publikacji atrybutów.", + "attributes-subscribe-topic-filter-required":"Wymagany jest filtr tematu subskrypcji atrybutów", + "telemetry-proto-schema":"Schemat protokołu telemetrii", + "telemetry-proto-schema-required":"Wymagany jest schemat protokołu telemetrii.", + "attributes-proto-schema":"Schemat protokołu atrybutów", + "attributes-proto-schema-required":"Wymagany jest schemat protokołu atrybutów.", + "rpc-response-proto-schema":"Schemat protokołu odpowiedzi RPC", + "rpc-response-proto-schema-required":"Wymagany jest schemat protokołu odpowiedzi RPC.", + "rpc-response-topic-filter":"Filtr tematu odpowiedzi RPC", + "rpc-response-topic-filter-required":"Wymagany jest filtr tematu odpowiedzi RPC.", + "rpc-request-proto-schema":"Schemat protokołu żądania RPC", + "rpc-request-proto-schema-required":"Wymagany jest schemat protokołu żądania RPC.", + "rpc-request-proto-schema-hint":"Wiadomość żądania RPC powinna zawsze zawierać pola: string method = 1; int32 requestId = 2; oraz params = 3 dowolnego typu danych.", + "not-valid-pattern-topic-filter":"Nieprawidłowy filtr tematu wzorca", + "not-valid-single-character":"Nieprawidłowe użycie znaku wieloznacznego jednego poziomu", + "not-valid-multi-character":"Nieprawidłowe użycie znaku wieloznacznego wielu poziomów", + "single-level-wildcards-hint":"[+] nadaje się do dowolnego poziomu filtra tematu. Na przykład: v1/devices/+/telemetry lub +/devices/+/attributes.", + "multi-level-wildcards-hint":"[#] może zastąpić sam filtr tematu i musi być ostatnim symbolem tematu. Na przykład: # lub v1/devices/me/#.", + "alarm-rules":"Zasady alarmowe", + "alarm-rules-with-count":"Zasady alarmowe ({{count}})", + "no-alarm-rules":"Brak skonfigurowanych zasad alarmowych", + "add-alarm-rule":"Dodaj zasadę alarmową", + "edit-alarm-rule":"Edytuj zasadę alarmową", + "alarm-type":"Typ alarmu", + "alarm-type-required":"Wymagany jest typ alarmu.", + "alarm-type-unique":"Typ alarmu musi być unikalny w ramach zasad alarmowych profilu urządzenia.", + "alarm-type-max-length":"Typ alarmu powinien być krótszy niż 256", + "create-alarm-pattern":"Utwórz alarm {{alarmType}}", + "create-alarm-rules":"Utwórz zasady alarmowe", + "no-create-alarm-rules":"Brak skonfigurowanych warunków utworzenia", + "add-create-alarm-rule-prompt":"Proszę dodać warunek utworzenia alarmu", + "clear-alarm-rule":"Wyczyść zasadę alarmową", + "no-clear-alarm-rule":"Brak skonfigurowanego warunku wyczyszczenia", + "add-create-alarm-rule":"Dodaj warunek utworzenia alarmu", + "add-clear-alarm-rule":"Dodaj warunek wyczyszczenia", + "select-alarm-severity":"Wybierz stopień pilności alarmu", + "alarm-severity-required":"Wymagany jest stopień pilności alarmu.", + "condition-duration":"Czas trwania warunku", + "condition-duration-value":"Wartość czasu trwania", + "condition-duration-time-unit":"Jednostka czasu", + "condition-duration-value-range":"Wartość czasu trwania powinna zawierać się w zakresie od 1 do 2147483647.", + "condition-duration-value-pattern":"Wartość czasu trwania powinna być liczbą całkowitą.", + "condition-duration-value-required":"Wymagana jest wartość czasu trwania.", + "condition-duration-time-unit-required":"Wymagana jest jednostka czasu.", + "advanced-settings":"Zaawansowane ustawienia", + "alarm-rule-additional-info":"Dodatkowe informacje", + "edit-alarm-rule-additional-info":"Edytuj dodatkowe informacje", + "alarm-rule-additional-info-placeholder":"Proszę podać swoje uwagi i dostosowania tutaj, aby wyświetlić je w szczegółach alarmu w sekcji Dodatkowe informacje", + "alarm-rule-additional-info-hint":"Wskazówka: użyj ${kluczNazwy} do zastąpienia wartości kluczy atrybutu lub telemetrii używanych w warunku zasady alarmowej.", + "alarm-rule-mobile-dashboard":"Panel nawigacyjny na urządzenia przenośne", + "alarm-rule-mobile-dashboard-hint":"Używane przez aplikację mobilną jako panel nawigacyjny szczegółów alarmu", + "alarm-rule-no-mobile-dashboard":"Nie wybrano panelu nawigacyjnego", + "propagate-alarm":"Rozprzestrzeniaj alarm do powiązanych jednostek", + "alarm-rule-relation-types-list":"Typy relacji do rozprzestrzeniania", + "alarm-rule-relation-types-list-hint":"Jeśli nie zostaną wybrane typy relacji do rozprzestrzeniania, alarmy będą rozprzestrzeniane bez filtrowania według typu relacji.", + "propagate-alarm-to-owner":"Rozprzestrzeniaj alarm do właściciela jednostki (Klienta lub Najemcy)", + "propagate-alarm-to-tenant":"Rozprzestrzeniaj alarm do Najemcy", + "alarm-rule-condition":"Warunek zasady alarmowej", + "enter-alarm-rule-condition-prompt":"Proszę dodać warunek zasady alarmowej", + "edit-alarm-rule-condition":"Edytuj warunek zasady alarmowej", + "device-provisioning":"Proces przydzielania urządzenia", + "provision-strategy":"Strategia przydzielania", + "provision-strategy-required":"Wymagana jest strategia przydzielania.", + "provision-strategy-disabled":"Wyłączone", + "provision-strategy-created-new":"Zezwól na tworzenie nowych urządzeń", + "provision-strategy-check-pre-provisioned":"Sprawdź wcześniej przydzielone urządzenia", + "provision-device-key":"Klucz przydzielania urządzenia", + "provision-device-key-required":"Wymagany jest klucz przydzielania urządzenia.", + "copy-provision-key":"Kopiuj klucz przydzielania", + "provision-key-copied-message":"Klucz przydzielania został skopiowany do schowka", + "provision-device-secret":"Sekret przydzielania urządzenia", + "provision-device-secret-required":"Wymagany jest sekret przydzielania urządzenia.", + "copy-provision-secret":"Kopiuj sekret przydzielania", + "provision-secret-copied-message":"Sekret przydzielania został skopiowany do schowka", + "provision-strategy-x509":{ + "certificate-chain":"Łańcuch certyfikatów X509", + "certificate-chain-hint":"Strategia certyfikatów X.509 jest używana do przydzielania urządzeń za pomocą certyfikatów klientów w dwukierunkowej komunikacji TLS.", + "allow-create-new-devices":"Utwórz nowe urządzenia", + "allow-create-new-devices-hint":"Jeśli zaznaczone, nowe urządzenia zostaną utworzone, a certyfikat klienta będzie używany jako dane uwierzytelniające urządzenia.", + "certificate-value":"Certyfikat w formacie PEM", + "certificate-value-required":"Wymagany jest certyfikat w formacie PEM", + "cn-regex-variable":"Zmienna wyrażenia regularnego CN", + "cn-regex-variable-required":"Wymagana jest zmienna wyrażenia regularnego CN", + "cn-regex-variable-hint":"Wymagane do pobrania nazwy urządzenia z wspólnego nazewnictwa certyfikatu X509 urządzenia." + }, + "condition":"Warunek", + "condition-type":"Typ warunku", + "condition-type-simple":"Prosty", + "condition-type-duration":"Czas trwania", + "condition-during":"Podczas {{during}}", + "condition-during-dynamic":"Podczas \"{{attribute}}\" ({{during}})", + "condition-type-repeating":"Powtarzający się", + "condition-type-required":"Wymagany jest typ warunku.", + "condition-repeating-value":"Liczba zdarzeń", + "condition-repeating-value-range":"Liczba zdarzeń powinna mieścić się w zakresie od 1 do 2147483647.", + "condition-repeating-value-pattern":"Liczba zdarzeń powinna być liczbą całkowitą.", + "condition-repeating-value-required":"Wymagana jest liczba zdarzeń.", + "condition-repeat-times":"Powtarza się { count, plural, =1 {1 raz} other {# razy} }", + "condition-repeat-times-dynamic":"Powtarza się \"{attribute}\" ({ count, plural, =1 {1 raz} other {# razy} })", + "schedule-type":"Typ harmonogramu", + "schedule-type-required":"Wymagany jest typ harmonogramu.", + "schedule":"Harmonogram", + "edit-schedule":"Edytuj harmonogram alarmu", + "schedule-any-time":"Aktywny przez cały czas", + "schedule-specific-time":"Aktywny o określonej godzinie", + "schedule-custom":"Niestandardowy", + "schedule-day":{ + "monday":"Poniedziałek", + "tuesday":"Wtorek", + "wednesday":"Środa", + "thursday":"Czwartek", + "friday":"Piątek", + "saturday":"Sobota", + "sunday":"Niedziela" + }, + "schedule-days":"Dni", + "schedule-time":"Czas", + "schedule-time-from":"Od", + "schedule-time-to":"Do", + "schedule-days-of-week-required":"Należy wybrać przynajmniej jeden dzień tygodnia.", + "create-device-profile":"Utwórz nowy profil urządzenia", + "import":"Importuj profil urządzenia", + "export":"Eksportuj profil urządzenia", + "export-failed-error":"Nie można wyeksportować profilu urządzenia: {{error}}", + "device-profile-file":"Plik profilu urządzenia", + "invalid-device-profile-file-error":"Nie można zaimportować profilu urządzenia: Nieprawidłowa struktura danych profilu urządzenia.", + "power-saving-mode":"Tryb oszczędzania energii", + "power-saving-mode-type":{ + "default":"Użyj trybu oszczędzania energii profilu urządzenia", + "psm":"Tryb oszczędzania energii (PSM)", + "drx":"Przerwane odbieranie (DRX)", + "edrx":"Rozszerzone przerwane odbieranie (eDRX)" + }, + "edrx-cycle":"Cykl eDRX", + "edrx-cycle-required":"Cykl eDRX jest wymagany.", + "edrx-cycle-pattern":"Cykl eDRX musi być liczbą całkowitą dodatnią.", + "edrx-cycle-min":"Minimalna liczba cykli eDRX to {{ min }} sekundy.", + "paging-transmission-window":"Okno transmisji strony", + "paging-transmission-window-required":"Okno transmisji strony jest wymagane.", + "paging-transmission-window-pattern":"Okno transmisji strony musi być liczbą całkowitą dodatnią.", + "paging-transmission-window-min":"Minimalna liczba okna transmisji strony to {{ min }} sekundy.", + "psm-activity-timer":"Licznik aktywności PSM", + "psm-activity-timer-required":"Licznik aktywności PSM jest wymagany.", + "psm-activity-timer-pattern":"Licznik aktywności PSM musi być liczbą całkowitą dodatnią.", + "psm-activity-timer-min":"Minimalna liczba licznika aktywności PSM to {{ min }} sekundy.", + "lwm2m":{ + "object-list":"Lista obiektów", + "object-list-empty":"Brak wybranych obiektów.", + "no-objects-found":"Nie znaleziono obiektów.", + "no-objects-matching":"Nie znaleziono obiektów pasujących do '{{object}}'.", + "model-tab":"Model LWM2M", + "add-new-instances":"Dodaj nowe instancje", + "instances-list":"Lista instancji", + "instances-list-required":"Lista instancji jest wymagana.", + "instance-id-pattern":"Identyfikator instancji musi być liczbą całkowitą dodatnią.", + "instance-id-max":"Maksymalna wartość identyfikatora instancji to {{max}}.", + "instance":"Instancja", + "resource-label":"#ID Nazwa zasobu", + "observe-label":"Obserwuj", + "attribute-label":"Atrybut", + "telemetry-label":"Telemetria", + "edit-observe-select":"Aby edytować obserwację, wybierz telemetrię lub atrybut", + "edit-attributes-select":"Aby edytować atrybuty, wybierz telemetrię lub atrybut", + "no-attributes-set":"Brak ustawionych atrybutów", + "key-name":"Nazwa klucza", + "key-name-required":"Nazwa klucza jest wymagana", + "attribute-name":"Nazwa atrybutu", + "attribute-name-required":"Nazwa atrybutu jest wymagana.", + "attribute-value":"Wartość atrybutu", + "attribute-value-required":"Wartość atrybutu jest wymagana.", + "attribute-value-pattern":"Wartość atrybutu musi być liczbą całkowitą dodatnią.", + "edit-attributes":"Edytuj atrybuty: {{ name }}", + "view-attributes":"Wyświetl atrybuty: {{ name }}", + "add-attribute":"Dodaj atrybut", + "edit-attribute":"Edytuj atrybut", + "view-attribute":"Wyświetl atrybut", + "remove-attribute":"Usuń atrybut", + "delete-server-text":"Bądź ostrożny, po potwierdzeniu konfiguracja serwera stanie się nieodwracalna.", + "delete-server-title":"Czy na pewno chcesz usunąć serwer?", + "mode":"Tryb konfiguracji zabezpieczeń", + "bootstrap-tab":"Bootstrap", + "bootstrap-server-legend":"Serwer Bootstrap (ShortId...)", + "lwm2m-server-legend":"Serwer LwM2M (ShortId...)", + "server":"Serwer", + "short-id":"Krótki identyfikator serwera", + "short-id-tooltip":"Krótki identyfikator serwera. Używany jako łącze do powiązania instancji obiektu serwera.\nTen identyfikator jednoznacznie identyfikuje każdy serwer LwM2M skonfigurowany dla klienta LwM2M.\nZasób MUSI być ustawiony, gdy Zasób Bootstrap-Server ma wartość „false”.\nWartości ID:0 i ID:65535 NIE MOGĄ być używane do identyfikacji serwera LwM2M.", + "short-id-required":"Krótki identyfikator serwera jest wymagany.", + "short-id-range":"Krótki identyfikator serwera powinien mieć wartość z zakresu od 1 do 65534.", + "short-id-pattern":"Krótki identyfikator serwera musi być liczbą całkowitą dodatnią.", + "lifetime":"Czas życia rejestracji klienta", + "lifetime-required":"Czas życia rejestracji klienta jest wymagany.", + "lifetime-pattern":"Czas życia rejestracji klienta musi być liczbą całkowitą dodatnią.", + "default-min-period":"Minimalny okres między dwoma powiadomieniami (s)", + "default-min-period-tooltip":"Domyślna wartość, jaką klient LwM2M powinien używać dla Minimalnego Okresu Obserwacji w przypadku braku uwzględnienia tego parametru w Obserwacji.", + "default-min-period-required":"Minimalny okres jest wymagany.", + "default-min-period-pattern":"Minimalny okres musi być liczbą całkowitą dodatnią.", + "notification-storing":"Przechowywanie powiadomień przy wyłączonym lub offline", + "binding":"Powiązanie", + "binding-type":{ + "u":"U: Klient jest dostępny za pośrednictwem powiązania UDP w dowolnym czasie.", + "m":"M: Klient jest dostępny za pośrednictwem powiązania MQTT w dowolnym czasie.", + "h":"H: Klient jest dostępny za pośrednictwem powiązania HTTP w dowolnym czasie.", + "t":"T: Klient jest dostępny za pośrednictwem powiązania TCP w dowolnym czasie.", + "s":"S: Klient jest dostępny za pośrednictwem powiązania SMS w dowolnym czasie.", + "n":"N: Klient MUSI wysłać odpowiedź na takie żądanie za pośrednictwem powiązania Non-IP (jest obsługiwane od LWM2M 1.1).", + "uq":"UQ: Połączenie UDP w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)", + "uqs":"UQS: zarówno połączenia UDP, jak i SMS aktywne; UDP w trybie kolejki, SMS w trybie standardowym (nie jest obsługiwane od LWM2M 1.1)", + "tq":"TQ: Połączenie TCP w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)", + "tqs":"TQS: zarówno połączenia TCP, jak i SMS aktywne; TCP w trybie kolejki, SMS w trybie standardowym (nie jest obsługiwane od LWM2M 1.1)", + "sq":"SQ: Połączenie SMS w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)" + }, + "binding-tooltip":"To jest lista w zasobie \"binding\" obiektu serwera LwM2M - /1/x/7. Wskazuje obsługiwane tryby powiązań w kliencie LwM2M. Ta wartość POWINNA być taka sama jak wartość w zasobie \"Supported Binding and Modes\" w obiekcie urządzenia (/3/0/16). Chociaż obsługiwane są różne środki transportu, tylko jedno powiązanie transportowe może być używane podczas całej sesji transportowej. Na przykład, gdy obsługiwane są zarówno UDP, jak i SMS, klient LwM2M i serwer LwM2M mogą wybrać komunikację zarówno przez UDP, jak i SMS przez całą sesję transportową.", + "bootstrap-server":"Serwer rozruchowy", + "lwm2m-server":"Serwer LwM2M", + "include-bootstrap-server":"Załącz aktualizacje serwera rozruchowego", + "bootstrap-update-title":"Masz już skonfigurowany serwer rozruchowy. Czy na pewno chcesz wyłączyć aktualizacje?", + "bootstrap-update-text":"Bądź ostrożny, po potwierdzeniu konfiguracja danych serwera rozruchowego stanie się nieodwracalna.", + "server-host":"Host", + "server-host-required":"Host jest wymagany.", + "server-port":"Port", + "server-port-required":"Port jest wymagany.", + "server-port-pattern":"Port musi być liczbą całkowitą dodatnią.", + "server-port-range":"Port powinien być w zakresie od 1 do 65535.", + "server-public-key":"Klucz publiczny serwera", + "server-public-key-required":"Klucz publiczny serwera jest wymagany.", + "client-hold-off-time":"Czas wstrzymania klienta", + "client-hold-off-time-required":"Czas wstrzymania klienta jest wymagany.", + "client-hold-off-time-pattern":"Czas wstrzymania klienta musi być liczbą całkowitą dodatnią.", + "client-hold-off-time-tooltip":"Czas wstrzymania klienta do użycia tylko z serwerem rozruchowym", + "account-after-timeout":"Konto po przekroczeniu czasu oczekiwania", + "account-after-timeout-required":"Konto po przekroczeniu czasu oczekiwania jest wymagane.", + "account-after-timeout-pattern":"Konto po przekroczeniu czasu oczekiwania musi być liczbą całkowitą dodatnią.", + "account-after-timeout-tooltip":"Wartość konta po przekroczeniu czasu oczekiwania podana przez ten zasób serwera rozruchowego.", + "server-type":"Typ serwera", + "add-new-server-title":"Dodaj nową konfigurację serwera", + "add-server-config":"Dodaj konfigurację serwera", + "add-lwm2m-server-config":"Dodaj serwer LwM2M", + "no-config-servers":"Brak skonfigurowanych serwerów", + "others-tab":"Inne ustawienia", + "client-strategy":"Strategia klienta podczas łączenia", + "client-strategy-label":"Strategia", + "client-strategy-only-observe":"Tylko żądanie obserwacji do klienta po połączeniu początkowym", + "client-strategy-read-all":"Odczytaj wszystkie zasoby i żądanie obserwacji do klienta po rejestracji", + "fw-update":"Aktualizacja oprogramowania", + "fw-update-strategy":"Strategia aktualizacji firmware'u", + "fw-update-strategy-data":"Przesuń aktualizację firmware'u jako plik binarny za pomocą Obiektu 19 i Zasobu 0 (Dane)", + "fw-update-strategy-package":"Przesuń aktualizację firmware'u jako plik binarny za pomocą Obiektu 5 i Zasobu 0 (Pakiet)", + "fw-update-strategy-package-uri":"Automatycznie generuj unikalny adres URL CoAP do pobrania pakietu i przesyłaj aktualizację firmware'u jako Obiekt 5 i Zasób 1 (Pakiet URI)", + "sw-update":"Aktualizacja oprogramowania", + "sw-update-strategy":"Strategia aktualizacji oprogramowania", + "sw-update-strategy-package":"Przesuń plik binarny za pomocą Obiektu 9 i Zasobu 2 (Pakiet)", + "sw-update-strategy-package-uri":"Automatycznie generuj unikalny adres URL CoAP do pobrania pakietu i przesyłaj aktualizację oprogramowania za pomocą Obiektu 9 i Zasób 3 (Pakiet URI)", + "fw-update-resource":"Zasób CoAP aktualizacji firmware'u", + "fw-update-resource-required":"Zasób CoAP aktualizacji firmware'u jest wymagany.", + "sw-update-resource":"Zasób CoAP aktualizacji oprogramowania", + "sw-update-resource-required":"Zasób CoAP aktualizacji oprogramowania jest wymagany.", + "config-json-tab":"Profil konfiguracji urządzenia Json", + "attributes-name":{ + "min-period":"Minimalny okres", + "max-period":"Maksymalny okres", + "greater-than":"Większe niż", + "less-than":"Mniejsze niż", + "step":"Krok", + "min-evaluation-period":"Minimalny okres oceny", + "max-evaluation-period":"Maksymalny okres oceny" + }, + "composite-operations-support":"Obsługuje operacje złożone Odczyt/Zapis/Obserwacja" + }, + "snmp":{ + "add-communication-config":"Dodaj konfigurację komunikacji", + "add-mapping":"Dodaj mapowanie", + "authentication-passphrase":"Hasło uwierzytelniania", + "authentication-passphrase-required":"Wymagane hasło uwierzytelniania.", + "authentication-protocol":"Protokół uwierzytelniania", + "authentication-protocol-required":"Wymagany protokół uwierzytelniania.", + "communication-configs":"Konfiguracje komunikacji", + "community":"Ciąg społeczności", + "community-required":"Wymagany ciąg społeczności.", + "context-name":"Nazwa kontekstu", + "data-key":"Klucz danych", + "data-key-required":"Wymagany klucz danych.", + "data-type":"Typ danych", + "data-type-required":"Wymagany typ danych.", + "engine-id":"ID silnika", + "host":"Host", + "host-required":"Wymagany host.", + "oid":"OID", + "oid-pattern":"Nieprawidłowy format OID.", + "oid-required":"Wymagany OID.", + "please-add-communication-config":"Proszę dodać konfigurację komunikacji", + "please-add-mapping-config":"Proszę dodać konfigurację mapowania", + "port":"Port", + "port-format":"Nieprawidłowy format portu.", + "port-required":"Wymagany port.", + "privacy-passphrase":"Hasło prywatności", + "privacy-passphrase-required":"Wymagane hasło prywatności.", + "privacy-protocol":"Protokół prywatności", + "privacy-protocol-required":"Wymagany protokół prywatności.", + "protocol-version":"Wersja protokołu", + "protocol-version-required":"Wymagana wersja protokołu.", + "querying-frequency":"Częstotliwość zapytań, ms", + "querying-frequency-invalid-format":"Częstotliwość zapytań musi być liczbą całkowitą dodatnią.", + "querying-frequency-required":"Wymagana częstotliwość zapytań.", + "retries":"Ponowienia", + "retries-invalid-format":"Liczba ponowień musi być liczbą całkowitą dodatnią.", + "retries-required":"Wymagane ponowienia.", + "scope":"Zakres", + "scope-required":"Wymagany zakres.", + "security-name":"Nazwa zabezpieczeń", + "security-name-required":"Wymagana nazwa zabezpieczeń.", + "timeout-ms":"Limit czasu, ms", + "timeout-ms-invalid-format":"Limit czasu musi być liczbą całkowitą dodatnią.", + "timeout-ms-required":"Wymagany limit czasu.", + "user-name":"Nazwa użytkownika", + "user-name-required":"Wymagana nazwa użytkownika." + } + }, + "dialog":{ + "close":"Zamknij okno dialogowe", + "error-message-title":"Wiadomość o błędzie:", + "error-details-title":"Szczegóły błędu" + }, + "direction":{ + "column":"Kolumna", + "row":"Wiersz" + }, + "edge":{ + "edge":"Krawędź", + "edge-instances":"Instancje krawędzi", + "instances":"Instancje", + "edge-file":"Plik krawędzi", + "name-max-length":"Nazwa powinna być krótsza niż 256", + "label-max-length":"Etykieta powinna być krótsza niż 256", + "type-max-length":"Typ powinien być krótszy niż 256", + "management":"Zarządzanie krawędzią", + "no-edges-matching":"Brak krawędzi pasujących do '{{entity}}'", + "add":"Dodaj krawędź", + "no-edges-text":"Brak znalezionych krawędzi", + "edge-details":"Szczegóły krawędzi", + "add-edge-text":"Dodaj nową krawędź", + "delete":"Usuń krawędź", + "delete-edge-title":"Czy na pewno chcesz usunąć krawędź '{{edgeName}}'?", + "delete-edge-text":"Bądź ostrożny, po potwierdzeniu krawędź i wszystkie związane z nią dane staną się nieodwracalnie utracone.", + "delete-edges-title":"Czy na pewno chcesz usunąć krawędź { count, plural, =1 {1 krawędź} other {# krawędzi} }?", + "delete-edges-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane krawędzie zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", + "name":"Nazwa", + "name-starts-with":"Nazwa krawędzi zaczyna się od", + "name-required":"Nazwa jest wymagana.", + "description":"Opis", + "details":"Szczegóły", + "events":"Zdarzenia", + "copy-id":"Kopiuj identyfikator krawędzi", + "id-copied-message":"Identyfikator krawędzi został skopiowany do schowka", + "sync":"Synchronizuj krawędź", + "edge-required":"Wymagana krawędź", + "edge-type":"Typ krawędzi", + "edge-type-required":"Wymagany jest typ krawędzi.", + "event-action":"Akcja zdarzenia", + "entity-id":"Identyfikator jednostki", + "select-edge-type":"Wybierz typ krawędzi", + "assign-to-customer":"Przypisz do klienta", + "assign-to-customer-text":"Proszę wybrać klienta, do którego przypisane będą krawędzie", + "assign-edge-to-customer":"Przypisz krawędź do klienta", + "assign-edge-to-customer-text":"Proszę wybrać krawędzie do przypisania do klienta", + "assignedToCustomer":"Przypisane do klienta", + "edge-public":"Krawędź jest publiczna", + "assigned-to-customer":"Przypisane do: {{customerTitle}}", + "unassign-from-customer":"Odłącz od klienta", + "unassign-edge-title":"Czy na pewno chcesz odłączyć krawędź '{{edgeName}}'?", + "unassign-edge-text":"Po potwierdzeniu krawędź zostanie odłączona i nie będzie dostępna dla klienta.", + "unassign-edges-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 krawędź} other {# krawędzie} }?", + "unassign-edges-text":"Po potwierdzeniu wszystkie wybrane krawędzie zostaną odłączone i nie będą dostępne dla klienta.", + "make-public":"Udostępnij publicznie", + "make-public-edge-title":"Czy na pewno chcesz udostępnić publicznie krawędź '{{edgeName}}'?", + "make-public-edge-text":"Po potwierdzeniu krawędź i wszystkie jej dane zostaną udostępnione publicznie i będą dostępne dla innych.", + "make-private":"Udostępnij prywatnie", + "public":"Publiczne", + "make-private-edge-title":"Czy na pewno chcesz udostępnić prywatnie krawędź '{{edgeName}}'?", + "make-private-edge-text":"Po potwierdzeniu krawędź i wszystkie jej dane zostaną udostępnione prywatnie i nie będą dostępne dla innych.", + "import":"Importuj krawędź", + "install-connect-instructions":"Instrukcje instalacji i połączenia", + "install-connect-instructions-edge-created":"Krawędź utworzona! Sprawdź instrukcje instalacji i połączenia", + "loading-edge-instructions":"Ładowanie instrukcji krawędzi...", + "label":"Etykieta", + "load-entity-error":"Nie udało się załadować danych. Jednostka została usunięta.", + "assign-new-edge":"Przypisz nową krawędź", + "unassign-from-edge":"Odłącz od krawędzi", + "edge-key":"Klucz krawędzi", + "copy-edge-key":"Kopiuj klucz krawędzi", + "edge-key-copied-message":"Klucz krawędzi został skopiowany do schowka", + "edge-secret":"Sekret krawędzi", + "copy-edge-secret":"Kopiuj sekret krawędzi", + "edge-secret-copied-message":"Sekret krawędzi został skopiowany do schowka", + "manage-assets":"Zarządzaj zasobami", + "manage-devices":"Zarządzaj urządzeniami", + "manage-entity-views":"Zarządzaj widokami jednostek", + "manage-dashboards":"Zarządzaj pulpitem nawigacyjnym", + "manage-rulechains":"Zarządzaj łańcuchami reguł", + "assets":"Zasoby krawędzi", + "devices":"Urządzenia krawędziowe", + "entity-views":"Widoki jednostek krawędziowych", + "dashboard":"Pulpit nawigacyjny krawędzi", + "dashboards":"Panele nawigacyjne krawędziowe", + "rulechain-templates":"Szablony łańcucha reguł", + "edge-rulechain-templates":"Szablony łańcucha reguł krawędziowych", + "rulechains":"Łańcuchy reguł krawędziowych", + "search":"Wyszukaj krawędzie", + "selected-edges":"{ count, plural, =1 {1 krawędź} other {# krawędzie} } wybrana", + "any-edge":"Dowolna krawędź", + "no-edge-types-matching":"Brak typów krawędzi pasujących do '{{entitySubtype}}'", + "edge-type-list-empty":"Brak wybranych typów krawędzi.", + "edge-types":"Typy krawędzi", + "enter-edge-type":"Wprowadź typ krawędzi", + "deployed":"Wdrożone", + "pending":"Oczekujące", + "downlinks":"Downlinki", + "no-downlinks-prompt":"Brak znalezionych downlinków", + "sync-process-started-successfully":"Proces synchronizacji został pomyślnie uruchomiony!", + "missing-related-rule-chains-title":"Krawędź ma brakujące powiązane łańcuchy reguł", + "missing-related-rule-chains-text":"Przypisane do krawędzi łańcuchy reguł używają węzłów reguł, które przekazują wiadomości do łańcuchów reguł, które nie są przypisane do tej krawędzi.

Lista brakujących łańcuchów reguł:
{{missingRuleChains}}", + "upgrade-instructions":"Instrukcje aktualizacji", + "widget-datasource-error":"Ten widżet obsługuje tylko źródło danych krawędziowe" + }, + "edge-event":{ + "type-dashboard":"Panel nawigacyjny", + "type-asset":"Zasób", + "type-device":"Urządzenie", + "type-device-profile":"Profil urządzenia", + "type-asset-profile":"Profil zasobu", + "type-entity-view":"Widok jednostki", + "type-alarm":"Alarm", + "type-rule-chain":"Łańcuch reguł", + "type-rule-chain-metadata":"Metadane łańcucha reguł", + "type-edge":"Krawędź", + "type-user":"Użytkownik", + "type-tenant":"Najemca", + "type-tenant-profile":"Profil najemcy", + "type-customer":"Klient", + "type-relation":"Relacja", + "type-widgets-bundle":"Pakiet widżetów", + "type-widgets-type":"Typ widżetu", + "type-admin-settings":"Ustawienia administratora", + "type-ota-package":"Pakiet OTA", + "type-queue":"Kolejka", + "action-type-added":"Dodane", + "action-type-deleted":"Usunięte", + "action-type-updated":"Zaktualizowane", + "action-type-post-attributes":"Opublikuj atrybuty", + "action-type-attributes-updated":"Zaktualizowane atrybuty", + "action-type-attributes-deleted":"Usunięte atrybuty", + "action-type-timeseries-updated":"Zaktualizowane serie czasowe", + "action-type-credentials-updated":"Zaktualizowane dane uwierzytelniające", + "action-type-assigned-to-customer":"Przypisane do klienta", + "action-type-unassigned-from-customer":"Odłączone od klienta", + "action-type-relation-add-or-update":"Dodaj lub aktualizuj relację", + "action-type-relation-deleted":"Usunięta relacja", + "action-type-rpc-call":"Wywołanie RPC", + "action-type-alarm-ack":"Potwierdzenie alarmu", + "action-type-alarm-clear":"Wyczyść alarm", + "action-type-alarm-assigned":"Przypisany alarm", + "action-type-alarm-unassigned":"Odłączony alarm", + "action-type-assigned-to-edge":"Przypisane do krawędzi", + "action-type-unassigned-from-edge":"Odłączone od krawędzi", + "action-type-credentials-request":"Żądanie danych uwierzytelniających", + "action-type-entity-merge-request":"Żądanie scalenia jednostki" + }, + "error":{ + "unable-to-connect":"Nie można połączyć się z serwerem! Sprawdź swoje połączenie internetowe.", + "unhandled-error-code":"Niestandardowy kod błędu: {{errorCode}}", + "unknown-error":"Nieznany błąd" + }, + "entity":{ + "entity":"Encja", + "entities":"Encje", + "entities-count":"Liczba encji", + "alarms-count":"Liczba alarmów", + "aliases":"Aliasy encji", + "aliases-short":"Aliasy", + "entity-alias":"Alias encji", + "unable-delete-entity-alias-title":"Nie można usunąć aliasu encji", + "unable-delete-entity-alias-text":"Alias encji '{{entityAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "duplicate-alias-error":"Znaleziono zduplikowany alias '{{alias}}'.
Aleasy encji muszą być unikalne w obrębie deski rozdzielczej.", + "missing-entity-filter-error":"Brakuje filtru dla aliasu '{{alias}}'.", + "configure-alias":"Skonfiguruj alias '{{alias}}'", + "alias":"Alias", + "alias-required":"Wymagany jest alias encji.", + "remove-alias":"Usuń alias encji", + "add-alias":"Dodaj alias encji", + "entity-list":"Lista encji", + "entity-type":"Typ encji", + "entity-types":"Typy encji", + "entity-type-list":"Lista typów encji", + "any-entity":"Dowolna encja", + "add-entity-type":"Dodaj typ encji", + "enter-entity-type":"Wprowadź typ encji", + "no-entities-matching":"Nie znaleziono encji pasujących do '{{entity}}'.", + "no-entity-types-matching":"Nie znaleziono typów encji pasujących do '{{entityType}}'.", + "name-starts-with":"Wyrażenie nazwy", + "help-text":"Użyj '%', zgodnie z potrzebą: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.", + "use-entity-name-filter":"Użyj filtra", + "entity-list-empty":"Nie wybrano żadnych encji.", + "entity-type-list-required":"Należy wybrać co najmniej jeden typ encji.", + "entity-name-filter-required":"Wymagany jest filtr nazwy encji.", + "entity-name-filter-no-entity-matched":"Nie znaleziono encji rozpoczynających się od '{{entity}}'.", + "all-subtypes":"Wszystkie", + "select-entities":"Wybierz encje", + "no-aliases-found":"Nie znaleziono aliasów.", + "no-alias-matching":"'{{alias}}' nie znaleziono.", + "create-new-alias":"Utwórz nowy!", + "create-new":"Utwórz nowy", + "key":"Klucz", + "key-name":"Nazwa klucza", + "no-keys-found":"Nie znaleziono kluczy.", + "no-key-matching":"'{{key}}' nie znaleziono.", + "create-new-key":"Utwórz nowy!", + "type":"Typ", + "type-required":"Wymagany jest typ encji.", + "type-device":"Urządzenie", + "type-devices":"Urządzenia", + "list-of-devices":"{ count, plural, =1 {Jedno urządzenie} other {Lista # urządzeń} }", + "device-name-starts-with":"Urządzenia, których nazwy zaczynają się od '{{prefix}}'", + "type-device-profile":"Profil urządzenia", + "type-device-profiles":"Profile urządzeń", + "clear-selected-profiles":"Wyczyść wybrane profile", + "list-of-device-profiles":"{ count, plural, =1 {Jeden profil urządzenia} other {Lista # profili urządzeń} }", + "device-profile-name-starts-with":"Profile urządzeń, których nazwy zaczynają się od '{{prefix}}'", + "type-asset-profile":"Profil zasobów", + "type-asset-profiles":"Profile zasobów", + "list-of-asset-profiles":"{ count, plural, =1 {Jeden profil zasobów} other {Lista # profili zasobów} }", + "asset-profile-name-starts-with":"Profile zasobów, których nazwy zaczynają się od '{{prefix}}'", + "type-asset":"Zasób", + "type-assets":"Zasoby", + "list-of-assets":"{ count, plural, =1 {Jeden zasób} other {Lista # zasobów} }", + "asset-name-starts-with":"Zasoby, których nazwy zaczynają się od '{{prefix}}'", + "type-entity-view":"Widok encji", + "type-entity-views":"Widoki encji", + "list-of-entity-views":"{ count, plural, =1 {Jeden widok encji} other {Lista # widoków encji} }", + "entity-view-name-starts-with":"Widoki encji, których nazwy zaczynają się od '{{prefix}}'", + "type-rule":"Reguła", + "type-rules":"Reguły", + "list-of-rules":"{ count, plural, =1 {Jedna reguła} other {Lista # reguł} }", + "rule-name-starts-with":"Reguły, których nazwy zaczynają się od '{{prefix}}'", + "type-plugin":"Wtyczka", + "type-plugins":"Wtyczki", + "list-of-plugins":"{ count, plural, =1 {Jedna wtyczka} other {Lista # wtyczek} }", + "plugin-name-starts-with":"Wtyczki, których nazwy zaczynają się od '{{prefix}}'", + "type-tenant":"Najemca", + "type-tenants":"Najemcy", + "list-of-tenants":"{ count, plural, =1 {Jeden najemca} other {Lista # najemców} }", + "tenant-name-starts-with":"Najemcy, których nazwy zaczynają się od '{{prefix}}'", + "type-tenant-profile":"Profil najemcy", + "type-tenant-profiles":"Profile najemcy", + "list-of-tenant-profiles":"{ count, plural, =1 {Jeden profil najemcy} other {Lista # profili najemcy} }", + "tenant-profile-name-starts-with":"Profile najemcy, których nazwy zaczynają się od '{{prefix}}'", + "type-customer":"Klient", + "type-customers":"Klienci", + "list-of-customers":"{ count, plural, =1 {Jeden klient} other {Lista # klientów} }", + "customer-name-starts-with":"Klienci, których nazwy zaczynają się od '{{prefix}}'", + "type-user":"Użytkownik", + "type-users":"Użytkownicy", + "list-of-users":"{ count, plural, =1 {Jeden użytkownik} other {Lista # użytkowników} }", + "user-name-starts-with":"Użytkownicy, których nazwy zaczynają się od '{{prefix}}'", + "type-dashboard":"Deska rozdzielcza", + "type-dashboards":"Deski rozdzielcze", + "list-of-dashboards":"{ count, plural, =1 {Jedna deska rozdzielcza} other {Lista # desek rozdzielczych} }", + "dashboard-name-starts-with":"Deski rozdzielcze, których nazwy zaczynają się od '{{prefix}}'", + "type-alarm":"Alarm", + "type-alarms":"Alarmy", + "list-of-alarms":"{ count, plural, =1 {Jeden alarm} other {Lista # alarmów} }", + "alarm-name-starts-with":"Alarmy, których nazwy zaczynają się od '{{prefix}}'", + "type-rulechain":"Łańcuch reguł", + "type-rulechains":"Łańcuchy reguł", + "list-of-rulechains":"{ count, plural, =1 {Jeden łańcuch reguł} other {Lista # łańcuchów reguł} }", + "rulechain-name-starts-with":"Łańcuchy reguł, których nazwy zaczynają się od '{{prefix}}'", + "type-rulenode":"Węzeł reguły", + "type-rulenodes":"Węzły reguł", + "list-of-rulenodes":"{ count, plural, =1 {Jeden węzeł reguły} other {Lista # węzłów reguł} }", + "rulenode-name-starts-with":"Węzły reguł, których nazwy zaczynają się od '{{prefix}}'", + "type-current-customer":"Bieżący Klient", + "type-current-tenant":"Bieżący Najemca", + "type-current-user":"Bieżący Użytkownik", + "type-current-user-owner":"Bieżący Właściciel Użytkownika", + "type-widgets-bundle":"Zestaw Widżetów", + "type-widgets-bundles":"Zestawy Widżetów", + "list-of-widgets-bundles":"{ count, plural, =1 {Jeden zestaw widżetów} other {Lista # zestawów widżetów} }", + "type-widget":"Widżet", + "type-widgets":"Widżety", + "list-of-widgets":"{ count, plural, =1 {Jeden widżet} other {Lista # widżetów} }", + "search":"Szukaj jednostek", + "selected-entities":"{ count, plural, =1 {1 jednostka} other {# jednostki} } wybranych", + "entity-name":"Nazwa jednostki", + "entity-label":"Etykieta jednostki", + "details":"Szczegóły jednostki", + "no-entities-prompt":"Nie znaleziono jednostek", + "no-data":"Brak danych do wyświetlenia", + "columns-to-display":"Kolumny do wyświetlenia", + "type-api-usage-state":"Stan korzystania z API", + "type-edge":"Edge", + "type-edges":"Edges", + "list-of-edges":"{ count, plural, =1 {Jeden edge} other {Lista # edges} }", + "edge-name-starts-with":"Edges, których nazwy zaczynają się od '{{prefix}}'", + "type-tb-resource":"Zasób", + "type-tb-resources":"Zasoby", + "list-of-tb-resources":"{ count, plural, =1 {Jeden zasób} other {Lista # zasobów} }", + "type-ota-package":"Pakiet OTA", + "type-rpc":"RPC", + "type-queue":"Kolejka", + "type-notification":"Powiadomienie", + "type-notification-rule":"Reguła powiadomień", + "type-notification-rules":"Reguły powiadomień", + "list-of-notification-rules":"{ count, plural, =1 {Jedna reguła powiadomień} other {Lista # reguł powiadomień} }", + "type-notification-target":"Odbiorca powiadomienia", + "type-notification-targets":"Odbiorcy powiadomień", + "list-of-notification-targets":"{ count, plural, =1 {Jeden odbiorca powiadomienia} other {Lista # odbiorców powiadomień} }", + "type-notification-request":"Żądanie powiadomienia", + "type-notification-template":"Szablon powiadomienia", + "type-notification-templates":"Szablony powiadomień", + "list-of-notification-templates":"{ count, plural, =1 {Jeden szablon powiadomienia} other {Lista # szablonów powiadomień} }" + }, + "entity-field":{ + "created-time":"Czas utworzenia", + "name":"Nazwa", + "type":"Typ", + "first-name":"Imię", + "last-name":"Nazwisko", + "email":"E-mail", + "title":"Tytuł", + "country":"Kraj", + "state":"Stan", + "city":"Miasto", + "address":"Adres", + "address2":"Adres 2", + "zip":"Kod pocztowy", + "phone":"Telefon", + "label":"Etykieta" + }, + "entity-view":{ + "entity-view":"Widok encji", + "entity-view-required":"Widok encji jest wymagany.", + "entity-views":"Widoki encji", + "management":"Zarządzanie widokiem encji", + "view-entity-views":"Wyświetl widoki encji", + "entity-view-alias":"Alias widoku encji", + "aliases":"Aliasy widoku encji", + "no-alias-matching":"'{{alias}}' nie znaleziono.", + "no-aliases-found":"Nie znaleziono aliasów.", + "no-key-matching":"'{{key}}' nie znaleziono.", + "no-keys-found":"Nie znaleziono kluczy.", + "create-new-alias":"Utwórz nowy!", + "create-new-key":"Utwórz nowy!", + "duplicate-alias-error":"Znaleziono zduplikowany alias '{{alias}}'.
Aliasy widoku encji muszą być unikalne w obrębie panelu.", + "configure-alias":"Konfiguruj alias '{{alias}}'", + "no-entity-views-matching":"Brak widoków encji pasujących do '{{entity}}'.", + "public":"Publiczny", + "alias":"Alias", + "alias-required":"Wymagany jest alias widoku encji.", + "remove-alias":"Usuń alias widoku encji", + "add-alias":"Dodaj alias widoku encji", + "name-starts-with":"Wyrażenie nazwy widoku encji", + "help-text":"Użyj '%' według potrzeb: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'.", + "entity-view-list":"Lista widoków encji", + "use-entity-view-name-filter":"Użyj filtra", + "entity-view-list-empty":"Brak wybranych widoków encji.", + "entity-view-name-filter-required":"Wymagany jest filtr nazwy widoku encji.", + "entity-view-name-filter-no-entity-view-matched":"Nie znaleziono widoków encji rozpoczynających się od '{{entityView}}'.", + "add":"Dodaj widok encji", + "entity-view-public":"Widok encji jest publiczny", + "assign-to-customer":"Przypisz do klienta", + "assign-entity-view-to-customer":"Przypisz Widoki Encji Do Klienta", + "assign-entity-view-to-customer-text":"Wybierz widoki encji do przypisania do klienta", + "assign-entity-view-to-edge-title":"Przypisz Widoki Encji Do Krawędzi", + "no-entity-views-text":"Nie znaleziono widoków encji", + "assign-to-customer-text":"Wybierz klienta, do którego chcesz przypisać widoki encji", + "entity-view-details":"Szczegóły widoku encji", + "add-entity-view-text":"Dodaj nowy widok encji", + "delete":"Usuń widok encji", + "assign-entity-views":"Przypisz widoki encji", + "assign-entity-views-text":"Przypisz { count, plural, =1 {1 widok encji} other {# widoków encji} } do klienta", + "delete-entity-views":"Usuń widoki encji", + "unassign-from-customer":"Odłącz od klienta", + "unassign-entity-views":"Odłącz widoki encji", + "unassign-entity-views-action-title":"Odłącz { count, plural, =1 {1 widok encji} other {# widoków encji} } od klienta", + "assign-new-entity-view":"Przypisz nowy widok encji", + "delete-entity-view-title":"Czy na pewno chcesz usunąć widok encji '{{entityViewName}}'?", + "delete-entity-view-text":"Bądź ostrożny, po potwierdzeniu widok encji i wszystkie związane z nim dane staną się nieodwracalnie utracone.", + "delete-entity-views-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 widok encji} other {# widoków encji} }?", + "delete-entity-views-action-title":"Usuń { count, plural, =1 {1 widok encji} other {# widoków encji} }", + "delete-entity-views-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane widoki encji zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", + "unassign-entity-view-title":"Czy na pewno chcesz odłączyć widok encji '{{entityViewName}}'?", + "unassign-entity-view-text":"Po potwierdzeniu widok encji zostanie odłączony i nie będzie dostępny dla klienta.", + "unassign-entity-view":"Odłącz widok encji", + "unassign-entity-views-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 widok encji} other {# widoków encji} }?", + "unassign-entity-views-text":"Po potwierdzeniu wszystkie wybrane widoki encji zostaną odłączone i nie będą dostępne dla klienta.", + "entity-view-type":"Typ widoku encji", + "entity-view-type-required":"Wymagany jest typ widoku encji.", + "select-entity-view-type":"Wybierz typ widoku encji", + "enter-entity-view-type":"Wprowadź typ widoku encji", + "any-entity-view":"Dowolny widok encji", + "no-entity-view-types-matching":"Brak typów widoków encji pasujących do '{{entitySubtype}}'.", + "entity-view-type-list-empty":"Brak wybranych typów widoków encji.", + "entity-view-types":"Typy widoków encji", + "created-time":"Czas utworzenia", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków.", + "type-max-length":"Typ widoku jednostki powinien mieć mniej niż 256 znaków.", + "description":"Opis", + "events":"Zdarzenia", + "details":"Szczegóły", + "copyId":"Kopiuj identyfikator widoku jednostki", + "idCopiedMessage":"Identyfikator widoku jednostki został skopiowany do schowka.", + "assignedToCustomer":"Przypisane do klienta", + "unable-entity-view-device-alias-title":"Nie można usunąć aliasu widoku jednostki", + "unable-entity-view-device-alias-text":"Alias urządzenia '{{entityViewAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widgety:
{{widgetsList}}", + "select-entity-view":"Wybierz widok jednostki", + "make-public":"Udostępnij widok jednostki publicznie", + "make-private":"Udostępnij widok jednostki prywatnie", + "start-date":"Data rozpoczęcia", + "start-ts":"Czas rozpoczęcia", + "end-date":"Data zakończenia", + "end-ts":"Czas zakończenia", + "date-limits":"Ograniczenia dat", + "client-attributes":"Atrybuty klienta", + "shared-attributes":"Współdzielone atrybuty", + "server-attributes":"Atrybuty serwera", + "timeseries":"Szereg czasowy", + "client-attributes-placeholder":"Atrybuty klienta", + "shared-attributes-placeholder":"Współdzielone atrybuty", + "server-attributes-placeholder":"Atrybuty serwera", + "timeseries-placeholder":"Szereg czasowy", + "target-entity":"Docelowa jednostka", + "attributes-propagation":"Propagacja atrybutów", + "attributes-propagation-hint":"Widok jednostki będzie automatycznie kopiować określone atrybuty z Docelowej Jednostki za każdym razem, gdy zapisujesz lub aktualizujesz ten widok jednostki. Ze względów wydajnościowych atrybuty Docelowej Jednostki nie są propagowane do widoku jednostki przy każdej zmianie atrybutu. Możesz włączyć automatyczną propagację, konfigurując węzeł reguły „kopiuj do widoku” w swoim łańcuchu reguł i łącząc wiadomości „Post attributes” i „Attributes Updated” z nowym węzłem reguły.", + "timeseries-data":"Dane szeregu czasowego", + "timeseries-data-hint":"Skonfiguruj klucze danych szeregu czasowego Docelowej Jednostki, które będą dostępne w widoku jednostki. Te dane szeregu czasowego są tylko do odczytu.", + "search":"Szukaj widoków jednostki", + "selected-entity-views":"{ count, plural, =1 {1 widok jednostki} other {# widoki jednostki} } wybrano", + "make-public-entity-view-title":"Czy na pewno chcesz udostępnić widok jednostki '{{entityViewName}}' publicznie?", + "make-public-entity-view-text":"Po potwierdzeniu widok jednostki i wszystkie jego dane staną się publiczne i dostępne dla innych.", + "make-private-entity-view-title":"Czy na pewno chcesz uczynić widok jednostki '{{entityViewName}}' prywatnym?", + "make-private-entity-view-text":"Po potwierdzeniu widok jednostki i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", + "assign-entity-view-to-edge":"Przypisz widok jednostki do urządzenia", + "assign-entity-view-to-edge-text":"Wybierz widoki jednostki do przypisania do urządzenia", + "unassign-entity-view-from-edge-title":"Czy na pewno chcesz odpiąć widok jednostki '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text":"Po potwierdzeniu widok jednostki zostanie odpięty i nie będzie dostępny dla urządzenia.", + "unassign-entity-views-from-edge-action-title":"Odpiąć { count, plural, =1 {1 widok jednostki} other {# widoki jednostki} } od urządzenia", + "unassign-entity-view-from-edge":"Odpiąć widok jednostki", + "unassign-entity-views-from-edge-title":"Czy na pewno chcesz odpiąć { count, plural, =1 {1 widok jednostki} other {# widoki jednostki} }?", + "unassign-entity-views-from-edge-text":"Po potwierdzeniu wszystkie wybrane widoki jednostki zostaną odpięte i nie będą dostępne dla urządzenia." + }, + "event":{ + "event-type":"Typ zdarzenia", + "events-filter":"Filtr zdarzeń", + "clean-events":"Wyczyść zdarzenia", + "type-error":"Błąd", + "type-lc-event":"Zdarzenie cyklu życia", + "type-stats":"Statystyki", + "type-debug-rule-node":"Debug", + "type-debug-rule-chain":"Debug", + "no-events-prompt":"Nie znaleziono zdarzeń", + "error":"Błąd", + "alarm":"Alarm", + "event-time":"Czas zdarzenia", + "server":"Serwer", + "body":"Treść", + "method":"Metoda", + "type":"Typ", + "message":"Wiadomość", + "message-id":"Identyfikator wiadomości", + "copy-message-id":"Kopiuj identyfikator wiadomości", + "message-type":"Typ wiadomości", + "data-type":"Typ danych", + "relation-type":"Typ relacji", + "metadata":"Metadane", + "data":"Dane", + "event":"Zdarzenie", + "status":"Status", + "success":"Sukces", + "failed":"Niepowodzenie", + "messages-processed":"Wiadomości przetworzone", + "max-messages-processed":"Maksymalna ilość wiadomości przetworzonych", + "min-messages-processed":"Minimalna ilość wiadomości przetworzonych", + "errors-occurred":"Wystąpiły błędy", + "max-errors-occurred":"Maksymalna ilość wystąpiłych błędów", + "min-errors-occurred":"Minimalna ilość wystąpiłych błędów", + "min-value":"Minimalna wartość to 0.", + "all-events":"Wszystkie", + "has-error":"Zawiera błąd", + "entity-id":"Identyfikator jednostki", + "copy-entity-id":"Kopiuj identyfikator jednostki", + "entity-type":"Typ jednostki", + "clear-filter":"Wyczyść filtr", + "clear-request-title":"Wyczyść wszystkie zdarzenia", + "clear-request-text":"Czy na pewno chcesz wyczyścić wszystkie zdarzenia?", + "started":"Rozpoczęte", + "updated":"Zaktualizowane", + "stopped":"Zatrzymane" + }, + "extension":{ + "extensions":"Rozszerzenia", + "selected-extensions":"{ count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} } wybranych", + "type":"Typ", + "key":"Klucz", + "value":"Wartość", + "id":"Identyfikator", + "extension-id":"Identyfikator rozszerzenia", + "extension-type":"Typ rozszerzenia", + "transformer-json":"JSON *", + "unique-id-required":"Identyfikator rozszerzenia już istnieje.", + "delete":"Usuń rozszerzenie", + "add":"Dodaj rozszerzenie", + "edit":"Edytuj rozszerzenie", + "delete-extension-title":"Czy na pewno chcesz usunąć rozszerzenie '{{extensionId}}'?", + "delete-extension-text":"Bądź ostrożny, po potwierdzeniu rozszerzenie i wszystkie związane z nim dane staną się nieodwracalnie utracone.", + "delete-extensions-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} }?", + "delete-extensions-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane rozszerzenia zostaną usunięte.", + "converters":"Konwertery", + "converter-id":"Identyfikator konwertera", + "configuration":"Konfiguracja", + "converter-configurations":"Konfiguracje konwertera", + "token":"Token bezpieczeństwa", + "add-converter":"Dodaj konwerter", + "add-config":"Dodaj konfigurację konwertera", + "device-name-expression":"Wyrażenie nazwy urządzenia", + "device-type-expression":"Wyrażenie typu urządzenia", + "custom":"Niestandardowe", + "to-double":"Do Double", + "transformer":"Transformator", + "json-required":"Wymagany jest JSON transformatora.", + "json-parse":"Nie można sparsować JSON transformatora.", + "attributes":"Atrybuty", + "add-attribute":"Dodaj atrybut", + "add-map":"Dodaj element mapowania", + "timeseries":"Szereg czasowy", + "add-timeseries":"Dodaj szereg czasowy", + "field-required":"Pole jest wymagane", + "brokers":"Brokerzy", + "add-broker":"Dodaj brokera", + "host":"Host", + "port":"Port", + "port-range":"Port powinien mieć zakres od 1 do 65535.", + "ssl":"SSL", + "credentials":"Dane uwierzytelniające", + "username":"Nazwa użytkownika", + "password":"Hasło", + "retry-interval":"Interwał ponawiania w milisekundach", + "anonymous":"Anonimowy", + "basic":"Podstawowy", + "pem":"PEM", + "ca-cert":"Plik certyfikatu CA *", + "private-key":"Plik klucza prywatnego *", + "cert":"Plik certyfikatu *", + "no-file":"Nie wybrano pliku.", + "drop-file":"Upuść plik lub kliknij, aby wybrać plik do przesłania.", + "mapping":"Mapowanie", + "topic-filter":"Filtr tematów", + "converter-type":"Typ konwertera", + "converter-json":"Json", + "json-name-expression":"Wyrażenie nazwy urządzenia JSON", + "topic-name-expression":"Wyrażenie nazwy tematu urządzenia", + "json-type-expression":"Wyrażenie typu urządzenia JSON", + "topic-type-expression":"Wyrażenie typu tematu urządzenia", + "attribute-key-expression":"Wyrażenie klucza atrybutu", + "attr-json-key-expression":"Wyrażenie klucza JSON atrybutu", + "attr-topic-key-expression":"Wyrażenie klucza tematu atrybutu", + "request-id-expression":"Wyrażenie identyfikatora żądania", + "request-id-json-expression":"Wyrażenie identyfikatora JSON żądania", + "request-id-topic-expression":"Wyrażenie identyfikatora tematu żądania", + "response-topic-expression":"Wyrażenie tematu odpowiedzi", + "value-expression":"Wyrażenie wartości", + "topic":"Temat", + "timeout":"Timeout w milisekundach", + "converter-json-required":"Wymagany jest JSON konwertera.", + "converter-json-parse":"Nie można sparsować JSON konwertera.", + "filter-expression":"Wyrażenie filtru", + "connect-requests":"Żądania połączenia", + "add-connect-request":"Dodaj żądanie połączenia", + "disconnect-requests":"Żądania rozłączenia", + "add-disconnect-request":"Dodaj żądanie rozłączenia", + "attribute-requests":"Żądania atrybutów", + "add-attribute-request":"Dodaj żądanie atrybutu", + "attribute-updates":"Aktualizacje atrybutów", + "add-attribute-update":"Dodaj aktualizację atrybutu", + "server-side-rpc":"RPC po stronie serwera", + "add-server-side-rpc-request":"Dodaj żądanie RPC po stronie serwera", + "device-name-filter":"Filtr nazwy urządzenia", + "attribute-filter":"Filtr atrybutu", + "method-filter":"Filtr metody", + "request-topic-expression":"Wyrażenie tematu żądania", + "response-timeout":"Timeout odpowiedzi w milisekundach", + "topic-expression":"Wyrażenie tematu", + "client-scope":"Zakres klienta", + "add-device":"Dodaj urządzenie", + "opc-server":"Serwery OPC", + "opc-add-server":"Dodaj serwer OPC", + "opc-add-server-prompt":"Proszę dodać serwer OPC", + "opc-application-name":"Nazwa aplikacji", + "opc-application-uri":"URI aplikacji", + "opc-scan-period-in-seconds":"Okres skanowania w sekundach", + "opc-security":"Bezpieczeństwo", + "opc-identity":"Tożsamość", + "opc-keystore":"Schronisko klucza", + "opc-type":"Typ", + "opc-keystore-type":"Typ schroniska klucza", + "opc-keystore-location":"Lokalizacja *", + "opc-keystore-password":"Hasło", + "opc-keystore-alias":"Alias", + "opc-keystore-key-password":"Hasło klucza", + "opc-device-node-pattern":"Wzorzec węzła urządzenia", + "opc-device-name-pattern":"Wzorzec nazwy urządzenia", + "modbus-server":"Serwery/slave'y Modbus", + "modbus-add-server":"Dodaj serwer/slave'a Modbus", + "modbus-add-server-prompt":"Proszę dodać serwer/slave'a Modbus", + "modbus-transport":"Transport", + "modbus-tcp-reconnect":"Automatyczne ponowne połączenie", + "modbus-rtu-over-tcp":"RTU przez TCP", + "modbus-port-name":"Nazwa portu szeregowego", + "modbus-encoding":"Kodowanie", + "modbus-parity":"Parzystość", + "modbus-baudrate":"Prędkość transmisji", + "modbus-databits":"Bity danych", + "modbus-stopbits":"Bity stopu", + "modbus-databits-range":"Bity danych powinny mieć zakres od 7 do 8.", + "modbus-stopbits-range":"Bity stopu powinny mieć zakres od 1 do 2.", + "modbus-unit-id":"Identyfikator jednostki", + "modbus-unit-id-range":"Identyfikator jednostki powinien mieć zakres od 1 do 247.", + "modbus-device-name":"Nazwa urządzenia", + "modbus-poll-period":"Okres sondowania (ms)", + "modbus-attributes-poll-period":"Okres sondowania atrybutów (ms)", + "modbus-timeseries-poll-period":"Okres sondowania szeregów czasowych (ms)", + "modbus-poll-period-range":"Okres sondowania powinien być wartością dodatnią.", + "modbus-tag":"Tag", + "modbus-function":"Funkcja", + "modbus-register-address":"Adres rejestru", + "modbus-register-address-range":"Adres rejestru powinien mieć zakres od 0 do 65535.", + "modbus-register-bit-index":"Indeks bitu", + "modbus-register-bit-index-range":"Indeks bitu powinien mieć zakres od 0 do 15.", + "modbus-register-count":"Liczba rejestrów", + "modbus-register-count-range":"Liczba rejestrów powinna być wartością dodatnią.", + "modbus-byte-order":"Kolejność bajtów", + "sync":{ + "status":"Status", + "sync":"Synchronizowane", + "not-sync":"Niesynchronizowane", + "last-sync-time":"Ostatnia synchronizacja", + "not-available":"Niedostępne" + }, + "export-extensions-configuration":"Eksportuj konfigurację rozszerzeń", + "import-extensions-configuration":"Importuj konfigurację rozszerzeń", + "import-extensions":"Importuj rozszerzenia", + "import-extension":"Importuj rozszerzenie", + "export-extension":"Eksportuj rozszerzenie", + "file":"Plik rozszerzeń", + "invalid-file-error":"Błąd - nieprawidłowy plik rozszerzenia" + }, + "feature":{ + "advanced-features":"Zaawansowane funkcje" + }, + "filter":{ + "add":"Dodaj filtr", + "edit":"Edytuj filtr", + "name":"Nazwa filtra", + "name-required":"Wymagana nazwa filtra.", + "duplicate-filter":"Filtr o tej samej nazwie już istnieje.", + "filters":"Filtry", + "unable-delete-filter-title":"Nie można usunąć filtra", + "unable-delete-filter-text":"Nie można usunąć filtra '{{filter}}', ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "duplicate-filter-error":"Znaleziono zduplikowany filtr '{{filter}}'.
Filtry muszą być unikalne w obrębie pulpitu nawigacyjnego.", + "missing-key-filters-error":"Kluczowe filtry są brakujące w filtrze '{{filter}}'.", + "filter":"Filtr", + "editable":"Możliwość edycji", + "no-filters-found":"Nie znaleziono filtrów.", + "no-filter-text":"Brak określonego filtra", + "add-filter-prompt":"Proszę dodać filtr", + "no-filter-matching":"'{{filter}}' nie znaleziono.", + "create-new-filter":"Utwórz nowy!", + "create-new":"Utwórz nowy", + "filter-required":"Filtr jest wymagany.", + "operation":{ + "operation":"Operacja", + "equal":"równa się", + "not-equal":"nie równa się", + "starts-with":"zaczyna się od", + "ends-with":"kończy się na", + "contains":"zawiera", + "not-contains":"nie zawiera", + "greater":"większa niż", + "less":"mniejsza niż", + "greater-or-equal":"większa lub równa", + "less-or-equal":"mniejsza lub równa", + "and":"i", + "or":"lub", + "in":"w", + "not-in":"nie w" + }, + "ignore-case":"ignoruj wielkość liter", + "value":"Wartość", + "remove-filter":"Usuń filtr", + "duplicate-filter-action":"Zduplikuj filtr", + "preview":"Podgląd filtra", + "no-filters":"Brak skonfigurowanych filtrów", + "add-filter":"Dodaj filtr", + "add-complex-filter":"Dodaj filtr złożony", + "add-complex":"Dodaj złożony", + "complex-filter":"Filtr złożony", + "edit-complex-filter":"Edytuj filtr złożony", + "edit-filter-user-params":"Edytuj parametry użytkownika predykatu filtra", + "filter-user-params":"Parametry użytkownika predykatu filtra", + "user-parameters":"Parametry użytkownika", + "display-label":"Etykieta do wyświetlenia", + "autogenerated-label":"Automatycznie generowana etykieta", + "order-priority":"Priorytet porządkowania pola", + "key-filter":"Filtr klucza", + "key-filters":"Filtry klucza", + "key-name":"Nazwa klucza", + "key-name-required":"Wymagana jest nazwa klucza.", + "key-type":{ + "key-type":"Typ klucza", + "attribute":"Atrybut", + "timeseries":"Szereg czasowy", + "entity-field":"Pole encji", + "constant":"Stała", + "client-attribute":"Atrybut klienta", + "server-attribute":"Atrybut serwera", + "shared-attribute":"Atrybut wspólny" + }, + "value-type":{ + "value-type":"Value type", + "string":"String", + "numeric":"Numeric", + "boolean":"Boolean", + "date-time":"Datetime" + }, + "value-type-required":"Wymagany jest typ wartości klucza.", + "key-value-type-change-title":"Czy na pewno chcesz zmienić typ wartości klucza?", + "key-value-type-change-message":"Jeśli potwierdzisz nowy typ wartości, wszystkie wprowadzone filtry klucza zostaną usunięte.", + "no-key-filters":"Brak skonfigurowanych filtrów klucza", + "add-key-filter":"Dodaj filtr klucza", + "remove-key-filter":"Usuń filtr klucza", + "edit-key-filter":"Edytuj filtr klucza", + "date":"Data", + "time":"Czas", + "current-tenant":"Bieżący najemca", + "current-customer":"Bieżący klient", + "current-user":"Bieżący użytkownik", + "current-device":"Bieżące urządzenie", + "default-value":"Wartość domyślna", + "dynamic-source-type":"Typ źródła dynamicznego", + "dynamic-value":"Wartość dynamiczna", + "no-dynamic-value":"Brak wartości dynamicznej", + "source-attribute":"Atrybut źródła", + "switch-to-dynamic-value":"Przełącz na wartość dynamiczną", + "switch-to-default-value":"Przełącz na wartość domyślną", + "inherit-owner":"Dziedzicz od właściciela", + "source-attribute-not-set":"Jeśli atrybut źródła nie jest ustawiony" + }, + "fullscreen":{ + "expand":"Rozwiń na pełny ekran", + "exit":"Wyjdź z trybu pełnoekranowego", + "toggle":"Przełącz tryb pełnoekranowy", + "fullscreen":"Pełny ekran" + }, + "function":{ + "function":"Funkcja" + }, + "gateway":{ + "add-entry":"Dodaj konfigurację", + "advanced":"Zaawansowane", + "checking-device-activity":"Sprawdzanie aktywności urządzenia", + "command":"Komendy Docker", + "command-copied-message":"Komenda Docker została skopiowana do schowka", + "configuration":"Konfiguracja", + "connector-add":"Dodaj nowy konektor", + "connector-enabled":"Włącz konektor", + "connector-name":"Nazwa konektora", + "connector-name-required":"Nazwa konektora jest wymagana.", + "connector-type":"Typ konektora", + "connector-type-required":"Typ konektora jest wymagany.", + "connectors":"Konektory", + "connectors-config":"Konfiguracja konektorów", + "connectors-table-enabled":"Włączony", + "connectors-table-name":"Nazwa", + "connectors-table-type":"Typ", + "connectors-table-status":"Status", + "connectors-table-actions":"Akcje", + "connectors-table-key":"Klucz", + "connectors-table-class":"Klasa", + "rpc-command-send":"Wyślij", + "rpc-command-result":"Odpowiedź", + "rpc-command-edit-params":"Edytuj parametry", + "gateway-configuration":"Konfiguracja ogólna", + "docker-label":"Użyj poniższej instrukcji, aby uruchomić bramę IoT w Docker Compose z uwierzytelnieniem dla wybranego urządzenia", + "install-docker-compose":"Skorzystaj z instrukcji, aby pobrać, zainstalować i skonfigurować Docker Compose", + "download-configuration-file":"Pobierz plik konfiguracyjny", + "download-docker-compose":"Pobierz plik docker-compose.yml dla Twojej bramy", + "launch-gateway":"Uruchom bramę", + "launch-docker-compose":"Uruchom bramę, używając poniższej komendy w terminalu z folderu zawierającego plik docker-compose.yml", + "create-new-gateway":"Utwórz nową bramę", + "create-new-gateway-text":"Jesteś pewien, że chcesz utworzyć nową bramę o nazwie: '{{gatewayName}}'?", + "created-time":"Czas utworzenia", + "configuration-delete-dialog-header":"Konfiguracje zostaną usunięte", + "configuration-delete-dialog-body":"Wyłączenie Konfiguracji Zdalnej jest możliwe tylko przy fizycznym dostępie do Bramy. Wszystkie wcześniejsze konfiguracje zostaną usunięte.

\nAby wyłączyć konfigurację, wprowadź poniżej nazwę Bramy", + "configuration-delete-dialog-input":"Nazwa Bramy", + "configuration-delete-dialog-input-required":"Nazwa Bramy jest obowiązkowa", + "configuration-delete-dialog-confirm":"Wyłącz", + "delete":"Usuń konfigurację", + "download-tip":"Pobierz plik konfiguracyjny", + "drop-file":"Upuść plik tutaj lub", + "gateway":"Brama", + "gateway-exists":"Urządzenie o tej samej nazwie już istnieje.", + "gateway-name":"Nazwa Bramy", + "gateway-name-required":"Nazwa Bramy jest wymagana.", + "gateway-saved":"Konfiguracja Bramy została pomyślnie zapisana.", + "grpc":"GRPC", + "grpc-keep-alive-timeout":"Limit czasu Keep Alive (w ms)", + "grpc-keep-alive-timeout-required":"Limit czasu Keep Alive jest wymagany", + "grpc-keep-alive-timeout-min":"Limit czasu Keep Alive nie może być mniejszy niż 1", + "grpc-keep-alive-timeout-pattern":"Limit czasu Keep Alive jest nieprawidłowy", + "grpc-keep-alive":"Keep Alive (w ms)", + "grpc-keep-alive-required":"Keep Alive jest wymagany", + "grpc-keep-alive-min":"Keep Alive nie może być mniejszy niż 1", + "grpc-keep-alive-pattern":"Keep Alive jest nieprawidłowy", + "grpc-min-time-between-pings":"Minimalny czas między pingami (w ms)", + "grpc-min-time-between-pings-required":"Minimalny czas między pingami jest wymagany", + "grpc-min-time-between-pings-min":"Minimalny czas między pingami nie może być mniejszy niż 1", + "grpc-min-time-between-pings-pattern":"Minimalny czas między pingami jest nieprawidłowy", + "grpc-min-ping-interval-without-data":"Minimalny interwał pingowania bez danych (w ms)", + "grpc-min-ping-interval-without-data-required":"Minimalny interwał pingowania bez danych jest wymagany", + "grpc-min-ping-interval-without-data-min":"Minimalny interwał pingowania bez danych nie może być mniejszy niż 1", + "grpc-min-ping-interval-without-data-pattern":"Minimalny interwał pingowania bez danych jest nieprawidłowy", + "grpc-max-pings-without-data":"Maksymalna liczba pingów bez danych", + "grpc-max-pings-without-data-required":"Maksymalna liczba pingów bez danych jest wymagana", + "grpc-max-pings-without-data-min":"Maksymalna liczba pingów bez danych nie może być mniejsza niż 1", + "grpc-max-pings-without-data-pattern":"Maksymalna liczba pingów bez danych jest nieprawidłowa", + "inactivity-check-period-seconds":"Okres sprawdzania braku aktywności (w sekundach)", + "inactivity-check-period-seconds-required":"Okres sprawdzania braku aktywności jest wymagany", + "inactivity-check-period-seconds-min":"Okres sprawdzania braku aktywności nie może być mniejszy niż 1", + "inactivity-check-period-seconds-pattern":"Okres sprawdzania braku aktywności jest nieprawidłowy", + "inactivity-timeout-seconds":"Okres braku aktywności (w sekundach)", + "inactivity-timeout-seconds-required":"Okres braku aktywności jest wymagany", + "inactivity-timeout-seconds-min":"Okres braku aktywności nie może być mniejszy niż 1", + "inactivity-timeout-seconds-pattern":"Okres braku aktywności jest nieprawidłowy", + "json-parse":"Nieprawidłowy format JSON.", + "json-required":"To pole nie może być puste.", + "logs":{ + "logs":"Logi", + "days":"dni", + "hours":"godzin", + "minutes":"minut", + "seconds":"sekund", + "date-format":"Format daty", + "date-format-required":"Wymagany format daty", + "log-format":"Format logów", + "log-type":"Typ logu", + "log-format-required":"Wymagany format logów", + "remote":"Zdalne logowanie", + "remote-logs":"Zdalne logi", + "local":"Lokalne logowanie", + "level":"Poziom logowania", + "file-path":"Ścieżka pliku", + "file-path-required":"Wymagana ścieżka pliku", + "saving-period":"Okres zapisywania logów", + "saving-period-min":"Okres zapisywania logów nie może być krótszy niż 1", + "saving-period-required":"Wymagany okres zapisywania logów", + "backup-count":"Liczba kopii zapasowych", + "backup-count-min":"Liczba kopii zapasowych nie może być mniejsza niż 1", + "backup-count-required":"Wymagana liczba kopii zapasowych" + }, + "min-pack-send-delay":"Minimalny opóźnienie wysyłki paczki (w ms)", + "min-pack-send-delay-required":"Wymagane minimalne opóźnienie wysyłki paczki", + "min-pack-send-delay-min":"Minimalne opóźnienie wysyłki paczki nie może być mniejsze niż 0", + "no-connectors":"Brak konektorów", + "no-data":"Brak konfiguracji", + "no-gateway-found":"Nie znaleziono bramy.", + "no-gateway-matching":"Brak dopasowania do '{{item}}'", + "path-logs":"Ścieżka do plików dziennika", + "path-logs-required":"Wymagana ścieżka", + "permit-without-calls":"Zezwalaj na keep alive bez wywołań", + "remote":"Zdalna konfiguracja", + "remote-logging-level":"Poziom logowania", + "remove-entry":"Usuń konfigurację", + "remote-shell":"Zdalny shell", + "remote-configuration":"Zdalna konfiguracja", + "other":"Inne", + "save-tip":"Zapisz plik konfiguracyjny", + "security-type":"Typ zabezpieczeń", + "security-types":{ + "access-token":"Token dostępu", + "username-password":"Nazwa użytkownika i hasło", + "tls":"TLS", + "tls-access-token":"TLS + Token dostępu", + "tls-private-key":"TLS + Klucz prywatny" + }, + "server-port":"Port serwera", + "statistics":{ + "statistic":"Statystyka", + "statistics":"Statystyki", + "statistic-commands-empty":"Brak dostępnych statystyk", + "commands":"Polecenia", + "send-period":"Okres wysyłania statystyk (w sekundach)", + "send-period-required":"Okres wysyłania statystyk jest wymagany", + "send-period-min":"Okres wysyłania statystyk nie może być krótszy niż 60", + "send-period-pattern":"Okres wysyłania statystyk jest nieprawidłowy", + "check-connectors-configuration":"Sprawdź konfigurację łączników (w sekundach)", + "check-connectors-configuration-required":"Sprawdzenie konfiguracji łączników jest wymagane", + "check-connectors-configuration-min":"Sprawdzenie konfiguracji łączników nie może być krótsze niż 1", + "check-connectors-configuration-pattern":"Sprawdzenie konfiguracji łączników jest nieprawidłowe", + "add":"Dodaj polecenie", + "timeout":"Limit czasu", + "timeout-ms":"Limit czasu (w ms)", + "timeout-required":"Limit czasu jest wymagany", + "timeout-min":"Limit czasu nie może być krótszy niż 1", + "timeout-pattern":"Limit czasu jest nieprawidłowy", + "attribute-name":"Nazwa atrybutu", + "attribute-name-required":"Nazwa atrybutu jest wymagana", + "command":"Polecenie", + "command-required":"Polecenie jest wymagane", + "command-pattern":"Polecenie jest nieprawidłowe", + "remove":"Usuń polecenie" + }, + "storage":"Magazyn", + "storage-max-file-records":"Maksymalna liczba rekordów w pliku", + "storage-max-files":"Maksymalna liczba plików", + "storage-max-files-min":"Minimalna liczba wynosi 1.", + "storage-max-files-pattern":"Liczba jest nieprawidłowa.", + "storage-max-files-required":"Wymagana jest liczba.", + "storage-max-records":"Maksymalna liczba rekordów w magazynie", + "storage-max-records-min":"Minimalna liczba rekordów wynosi 1.", + "storage-max-records-pattern":"Liczba jest nieprawidłowa.", + "storage-max-records-required":"Wymagana jest maksymalna liczba rekordów.", + "storage-read-record-count":"Liczba odczytanych rekordów w magazynie", + "storage-read-record-count-min":"Minimalna liczba rekordów wynosi 1.", + "storage-read-record-count-pattern":"Liczba jest nieprawidłowa.", + "storage-read-record-count-required":"Wymagana jest liczba odczytanych rekordów.", + "storage-max-read-record-count":"Maksymalna liczba odczytanych rekordów w magazynie", + "storage-max-read-record-count-min":"Minimalna liczba rekordów wynosi 1.", + "storage-max-read-record-count-pattern":"Liczba jest nieprawidłowa.", + "storage-max-read-record-count-required":"Wymagana jest maksymalna liczba odczytanych rekordów.", + "storage-data-folder-path":"Ścieżka folderu danych", + "storage-data-folder-path-required":"Wymagana jest ścieżka folderu danych.", + "storage-pack-size":"Maksymalny rozmiar pakietu zdarzeń", + "storage-pack-size-min":"Minimalna liczba wynosi 1.", + "storage-pack-size-pattern":"Liczba jest nieprawidłowa.", + "storage-pack-size-required":"Wymagany jest maksymalny rozmiar pakietu zdarzeń.", + "storage-path":"Ścieżka magazynu", + "storage-path-required":"Wymagana jest ścieżka magazynu.", + "storage-type":"Typ magazynu", + "storage-types":{ + "file-storage":"Magazyn plików", + "memory-storage":"Magazyn pamięci", + "sqlite":"SQLite" + }, + "thingsboard":"ThingsBoard", + "general":"Ogólne", + "thingsboard-host":"Host ThingsBoard", + "thingsboard-host-required":"Host jest wymagany.", + "thingsboard-port":"Port ThingsBoard", + "thingsboard-port-max":"Maksymalny numer portu to 65535.", + "thingsboard-port-min":"Minimalny numer portu to 1.", + "thingsboard-port-pattern":"Port jest niepoprawny.", + "thingsboard-port-required":"Port jest wymagany.", + "tidy":"Porządkuj", + "tidy-tip":"Porządkuj konfigurację JSON", + "title-connectors-json":"Konfiguracja konektora {{typeName}}", + "tls-path-ca-certificate":"Ścieżka do certyfikatu CA na gatewayu", + "tls-path-client-certificate":"Ścieżka do certyfikatu klienta na gatewayu", + "messages-ttl-check-in-hours":"Sprawdź TTL wiadomości w godzinach", + "messages-ttl-check-in-hours-required":"Sprawdzenie TTL wiadomości w godzinach jest wymagane.", + "messages-ttl-check-in-hours-min":"Minimalna liczba to 1.", + "messages-ttl-check-in-hours-pattern":"Liczba jest niepoprawna.", + "messages-ttl-in-days":"TTL wiadomości w dniach", + "messages-ttl-in-days-required":"TTL wiadomości w dniach jest wymagane.", + "messages-ttl-in-days-min":"Minimalna liczba to 1.", + "messages-ttl-in-days-pattern":"Liczba jest niepoprawna.", + "mqtt-qos":"QoS", + "mqtt-qos-required":"QoS jest wymagane", + "mqtt-qos-range":"Wartości QoS są w zakresie od 0 do 1", + "tls-path-private-key":"Ścieżka do prywatnego klucza na gatewayu", + "toggle-fullscreen":"Przełącz pełny ekran", + "transformer-json-config":"Konfiguracja JSON*", + "update-config":"Dodaj/aktualizuj konfigurację JSON", + "hints":{ + "remote-configuration":"Umożliwia zdalną konfigurację i zarządzanie bramką", + "remote-shell":"Umożliwia zdalne sterowanie systemem operacyjnym bramki z widżetu Zdalnej Powłoki", + "host":"Nazwa hosta lub adres IP serwera ThingsBoard", + "port":"Port usługi MQTT na serwerze ThingsBoard", + "token":"Token dostępowy dla bramki na serwerze ThingsBoard", + "client-id":"Identyfikator klienta MQTT dla bramki na serwerze ThingsBoard", + "username":"Nazwa użytkownika MQTT dla bramki na serwerze ThingsBoard", + "password":"Hasło MQTT dla bramki na serwerze ThingsBoard", + "ca-cert":"Ścieżka do pliku certyfikatu CA", + "date-form":"Format daty w komunikatach dziennika", + "data-folder":"Ścieżka do folderu, który będzie zawierał dane (względna lub bezwzględna)", + "log-format":"Format komunikatu dziennika", + "remote-log":"Umożliwia zdalne logowanie i odczyt dziennika z bramki", + "backup-count":"Jeśli liczba kopii zapasowych > 0, po dokonaniu zmiany rolki, nie więcej niż określona liczba plików kopii zapasowych jest przechowywana - najstarsze zostaną usunięte", + "storage":"Zapewnia konfigurację zapisu przychodzących danych przed ich wysłaniem na platformę", + "max-file-count":"Maksymalna liczba plików, które zostaną utworzone", + "max-read-count":"Liczba wiadomości do pobrania z magazynu i wysłania do ThingsBoard", + "max-records":"Maksymalna liczba rekordów przechowywanych w jednym pliku", + "read-record-count":"Liczba wiadomości do pobrania z magazynu i wysłania do ThingsBoard", + "max-records-count":"Maksymalna liczba danych w magazynie przed wysłaniem do ThingsBoard", + "ttl-check-hour":"Jak często brama sprawdza dane pod kątem przestarzałości", + "ttl-messages-day":"Maksymalna liczba dni, przez które dane będą przechowywane w magazynie", + "commands":"Polecenia do zbierania dodatkowych statystyk", + "attribute":"Klucz telemetrii statystyk", + "timeout":"Limit czasu na wykonanie polecenia", + "command":"Wynik wykonania polecenia, będzie używany jako wartość telemetrii", + "check-device-activity":"Umożliwia monitorowanie aktywności każdego podłączonego urządzenia", + "inactivity-timeout":"Czas po którym brama rozłączy urządzenie", + "inactivity-period":"Okresowość sprawdzania aktywności urządzenia", + "minimal-pack-delay":"Opóźnienie między wysyłką paczek wiadomości (zmniejszenie tego ustawienia zwiększa zużycie procesora)", + "qos":"Jakość usługi w komunikacji MQTT (0 - przynajmniej raz, 1 - co najmniej raz)", + "server-port":"Port sieciowy, na którym serwer GRPC nasłuchuje nadchodzących połączeń.", + "grpc-keep-alive-timeout":"Maksymalny czas oczekiwania serwera na odpowiedź keepalive ping przed uznaniem połączenia za martwe.", + "grpc-keep-alive":"Czas między dwoma kolejnymi wiadomościami keepalive ping, gdy nie ma aktywnego wywołania RPC.", + "grpc-min-time-between-pings":"Minimalny czas oczekiwania serwera między wysłaniem wiadomości keepalive ping.", + "grpc-max-pings-without-data":"Maksymalna liczba wiadomości keepalive ping, które serwer może wysłać, nie odbierając żadnych danych, zanim uzna połączenie za martwe.", + "grpc-min-ping-interval-without-data":"Minimalny czas oczekiwania serwera między wysłaniem wiadomości keepalive ping, gdy nie są przesyłane ani odbierane żadne dane.", + "permit-without-calls":"Zezwalaj serwerowi na utrzymanie połączenia GRPC aktywnego, nawet gdy nie ma aktywnych wywołań RPC." + } + }, + "grid":{ + "delete-item-title":"Czy na pewno chcesz usunąć ten element?", + "delete-item-text":"Bądź ostrożny, po potwierdzeniu ten element i wszystkie powiązane dane staną się nieodwracalne.", + "delete-items-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 element} other {# elementów} }?", + "delete-items-action-title":"Usuń { count, plural, =1 {1 element} other {# elementów} }", + "delete-items-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane elementy zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", + "add-item-text":"Dodaj nowy element", + "no-items-text":"Nie znaleziono elementów", + "item-details":"Szczegóły elementu", + "delete-item":"Usuń element", + "delete-items":"Usuń elementy", + "scroll-to-top":"Przewiń do góry" + }, + "help":{ + "goto-help-page":"Przejdź do strony pomocy", + "show-help":"Pokaż pomoc" + }, + "home":{ + "home":"Strona główna", + "profile":"Profil", + "logout":"Wyloguj się", + "menu":"Menu", + "avatar":"Awatar", + "open-user-menu":"Otwórz menu użytkownika" + }, + "file-input":{ + "browse-file":"Przeglądaj plik", + "browse-files":"Przeglądaj pliki" + }, + "image":{ + "gallery":"Galeria obrazów", + "search":"Wyszukaj obraz", + "selected-images":"{ count, plural, =1 {1 obraz} other {# obrazy} } wybranych", + "created-time":"Czas utworzenia", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "resolution":"Rozdzielczość", + "size":"Rozmiar", + "system":"System", + "download-image":"Pobierz obraz", + "export-image":"Eksportuj obraz do JSON", + "import-image":"Importuj obraz z JSON", + "upload-image":"Prześlij obraz", + "edit-image":"Edytuj obraz", + "image-details":"Szczegóły obrazu", + "no-images":"Nie znaleziono obrazów", + "delete-image":"Usuń obraz", + "delete-image-title":"Czy na pewno chcesz usunąć obraz '{{imageTitle}}'?", + "delete-image-text":"Bądź ostrożny, po potwierdzeniu obraz stanie się nieodwracalnie utracony.", + "delete-images-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 obraz} other {# obrazy} }?", + "delete-images-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane obrazy zostaną usunięte, a wszelkie powiązane dane staną się nieodwracalnie utracone.", + "list-mode":"Widok listy", + "grid-mode":"Widok siatki", + "image-preview":"Podgląd obrazu", + "update-image":"Aktualizuj obraz", + "export-failed-error":"Nie można wyeksportować obrazu: {{error}}", + "image-json-file":"Plik JSON obrazu", + "invalid-image-json-file-error":"Nie można zaimportować obrazu z JSON: Nieprawidłowa struktura danych JSON obrazu.", + "image-is-in-use":"Obraz jest używany przez inne jednostki", + "images-are-in-use":"Obrazy są używane przez inne jednostki", + "image-is-in-use-text":"Obraz '{{title}}' nie został usunięty, ponieważ jest używany przez następujące jednostki:", + "images-are-in-use-text":"Nie wszystkie obrazy zostały usunięte, ponieważ są używane przez inne jednostki.
Możesz zobaczyć odwołane jednostki, klikając przycisk Odwołania w odpowiednim wierszu obrazu.
Jeśli nadal chcesz usunąć te obrazy, zaznacz je w tabeli poniżej i kliknij przycisk Usuń zaznaczone.", + "delete-image-in-use-text":"Jeśli nadal chcesz usunąć obraz, kliknij przycisk Usuń tak czy inaczej.", + "system-entities":"Jednostki systemowe:", + "entities":"jednostki:", + "references":"Odwołania", + "include-system-images":"Dołącz obrazy systemowe", + "clear-image":"Wyczyść obraz", + "no-image":"Brak obrazu", + "no-image-selected":"Nie wybrano obrazu", + "browse-from-gallery":"Przeglądaj z galerii", + "set-link":"Ustaw link", + "image-link":"Link obrazu", + "link":"Link", + "copy-image-link":"Kopiuj link obrazu", + "embed-image":"Osadź obraz", + "embed-to-html":"Osadź w HTML", + "embed-to-html-hint":"Ta funkcja umożliwia udostępnianie linku każdemu nieautoryzowanemu użytkownikowi.", + "embed-to-html-text":"Za pomocą poniższego fragmentu kodu możesz osadzić obraz w komponentach opartych na prostym HTML.
Takie komponenty obejmują widżety karty HTML, funkcje zawartości komórek itp.", + "embed-to-angular-template":"Osadź w szablonie Angular HTML", + "embed-to-angular-template-text":"Za pomocą poniższego fragmentu kodu możesz osadzić obraz w szablonie Angular HTML.
Takie komponenty obejmują widżet Markdown, sekcję HTML w edytorze widżetów, niestandardowe akcje itp." + }, + "image-input":{ + "drop-images-or":"Przeciągnij i upuść obrazy lub", + "drag-and-drop":"Przeciągnij i Upuść", + "or":"lub", + "browse":"Przeglądaj", + "no-images":"Brak wybranych obrazów", + "images":"obrazy" + }, + "import":{ + "no-file":"Nie wybrano pliku", + "drop-file":"Przeciągnij plik JSON lub kliknij, aby wybrać plik do przesłania.", + "drop-json-file-or":"Przeciągnij plik JSON lub", + "drop-file-csv":"Przeciągnij plik CSV lub kliknij, aby wybrać plik do przesłania.", + "drop-file-csv-or":"Przeciągnij plik CSV lub", + "column-value":"Wartość", + "column-title":"Tytuł", + "column-example":"Przykładowa wartość danych", + "column-key":"Atrybut/klucz telemetryczny", + "credentials":"Dane uwierzytelniające", + "csv-delimiter":"Separator CSV", + "csv-first-line-header":"Pierwsza linia zawiera nazwy kolumn", + "csv-update-data":"Aktualizuj atrybuty/telemetrię", + "details":"Szczegóły", + "import-csv-number-columns-error":"Plik powinien zawierać co najmniej dwie kolumny", + "import-csv-invalid-format-error":"Nieprawidłowy format pliku. Linia: '{{line}}'", + "column-type":{ + "name":"Nazwa", + "type":"Typ", + "label":"Etykieta", + "column-type":"Typ kolumny", + "client-attribute":"Atrybut klienta", + "shared-attribute":"Wspólny atrybut", + "server-attribute":"Atrybut serwera", + "timeseries":"Szereg czasowy", + "entity-field":"Pole encji", + "access-token":"Token dostępu", + "x509":"X.509", + "mqtt":{ + "client-id":"ID klienta MQTT", + "user-name":"Nazwa użytkownika MQTT", + "password":"Hasło MQTT" + }, + "lwm2m":{ + "client-endpoint":"Nazwa klienta LwM2M endpoint", + "security-config-mode":"Tryb konfiguracji bezpieczeństwa LwM2M", + "client-identity":"Tożsamość klienta LwM2M", + "client-key":"Klucz klienta LwM2M", + "client-cert":"Certyfikat publiczny klienta LwM2M", + "bootstrap-server-security-mode":"Tryb bezpieczeństwa serwera rozruchowego LwM2M", + "bootstrap-server-secret-key":"Tajny klucz serwera rozruchowego LwM2M", + "bootstrap-server-public-key-id":"Klucz publiczny lub identyfikator serwera rozruchowego LwM2M", + "lwm2m-server-security-mode":"Tryb bezpieczeństwa serwera LwM2M", + "lwm2m-server-secret-key":"Tajny klucz serwera LwM2M", + "lwm2m-server-public-key-id":"Klucz publiczny lub identyfikator serwera LwM2M" + }, + "snmp":{ + "host":"Host SNMP", + "port":"Port SNMP", + "version":"Wersja SNMP (v1, v2c lub v3)", + "community-string":"Ciąg społeczności SNMP" + }, + "isgateway":"Czy to jest Bramą", + "activity-time-from-gateway-device":"Czas aktywności urządzenia bramowego", + "description":"Opis", + "routing-key":"Klucz brzegowy", + "secret":"Sekret brzegowy" + }, + "stepper-text":{ + "select-file":"Wybierz plik", + "configuration":"Importuj konfigurację", + "column-type":"Wybierz typy kolumn", + "creat-entities":"Tworzenie nowych jednostek" + }, + "message":{ + "create-entities":"{{count}} nowych jednostek zostało pomyślnie utworzonych.", + "update-entities":"{{count}} jednostek zostało pomyślnie zaktualizowanych.", + "error-entities":"Wystąpił błąd podczas tworzenia {{count}} jednostek." + } + }, + "item":{ + "selected":"Wybrane" + }, + "js-func":{ + "no-return-error":"Funkcja musi zwrócić wartość!", + "return-type-mismatch":"Funkcja musi zwrócić wartość typu '{{type}}'!", + "tidy":"Porządek", + "mini":"Mini" + }, + "key-val":{ + "key":"Klucz", + "value":"Wartość", + "remove-entry":"Usuń wpis", + "add-entry":"Dodaj wpis", + "no-data":"Brak danych" + }, + "layout":{ + "layout":"Układ", + "layouts":"Układy", + "manage":"Zarządzaj układami", + "settings":"Ustawienia układu", + "color":"Kolor", + "main":"Główny", + "right":"Prawo", + "left":"Lewo", + "select":"Wybierz docelowy układ", + "percentage-width":"Szerokość procentowa (%)", + "fixed-width":"Szerokość stała (px)", + "left-width":"Szerokość lewej kolumny (%)", + "right-width":"Szerokość prawej kolumny (%)", + "pick-fixed-side":"Stała strona: ", + "layout-fixed-width":"Szerokość stała (px)", + "value-min-error":"Wartość musi być większa niż {{min}}{{unit}}", + "value-max-error":"Wartość musi być mniejsza niż {{max}}{{unit}}", + "layout-fixed-width-required":"Szerokość stała jest wymagana", + "right-width-percentage-required":"Wymagany jest procent szerokości prawej strony", + "left-width-percentage-required":"Wymagany jest procent szerokości lewej strony", + "divider":"Podziałka", + "right-side":"Układ prawej strony", + "left-side":"Układ lewej strony" + }, + "legend":{ + "direction":"Kierunek", + "position":"Pozycja", + "show-values":"Pokaż wartości", + "min-option":"Min", + "max-option":"Maks.", + "average-option":"Średnia", + "total-option":"Suma", + "latest-option":"Najnowsza", + "sort-legend":"Sortuj klucze danych w legendzie", + "show-max":"Pokaż maksymalną wartość", + "show-min":"Pokaż minimalną wartość", + "show-avg":"Pokaż średnią wartość", + "show-total":"Pokaż sumę wartości", + "show-latest":"Pokaż najnowszą wartość", + "settings":"Ustawienia legendy", + "min":"min", + "max":"maks.", + "avg":"śr.", + "total":"suma", + "latest":"najnowsza", + "comparison-time-ago":{ + "previousInterval":"(poprzedni przedział)", + "customInterval":"(niestandardowy przedział)", + "days":"(dzień temu)", + "weeks":"(tydzień temu)", + "months":"(miesiąc temu)", + "years":"(rok temu)" + }, + "label":"Etykieta", + "value":"Wartość" + }, + "login":{ + "login":"Zaloguj się", + "request-password-reset":"Zażądaj resetu hasła", + "reset-password":"Resetuj hasło", + "create-password":"Utwórz hasło", + "two-factor-authentication":"Autentykacja dwuskładnikowa", + "passwords-mismatch-error":"Wprowadzone hasła muszą być takie same!", + "password-again":"Powtórz hasło", + "sign-in":"Proszę się zalogować", + "username":"Nazwa użytkownika (e-mail)", + "remember-me":"Zapamiętaj mnie", + "forgot-password":"Zapomniałeś hasła?", + "password-reset":"Reset hasła", + "expired-password-reset-message":"Twoje dane uwierzytelniające wygasły! Proszę utworzyć nowe hasło.", + "new-password":"Nowe hasło", + "new-password-again":"Potwierdź nowe hasło", + "password-link-sent-message":"Link do resetowania hasła został wysłany", + "email":"E-mail", + "login-with":"Zaloguj się za pomocą {{name}}", + "or":"lub", + "error":"Błąd logowania", + "verify-your-identity":"Zweryfikuj swoją tożsamość", + "select-way-to-verify":"Wybierz sposób weryfikacji", + "resend-code":"Wyślij kod ponownie", + "resend-code-wait":"Ponownie wyślij kod za { time, plural, =1 {1 sekundę} other {# sekundy} }", + "try-another-way":"Spróbuj inną metodę", + "totp-auth-description":"Proszę podać kod z aplikacji autoryzacyjnej.", + "totp-auth-placeholder":"Kod", + "sms-auth-description":"Kod zabezpieczający został wysłany na Twój telefon pod numerem {{contact}}.", + "sms-auth-placeholder":"Kod SMS", + "email-auth-description":"Kod zabezpieczający został wysłany na Twój adres e-mailowy {{contact}}.", + "email-auth-placeholder":"Kod e-mailowy", + "backup-code-auth-description":"Proszę podać jeden z kodów zapasowych.", + "backup-code-auth-placeholder":"Kod zapasowy" + }, + "markdown":{ + "edit":"Edytuj", + "preview":"Podgląd", + "copy-code":"Kliknij, aby skopiować", + "copied":"Skopiowano!" + }, + "notification":{ + "action-button":"Przycisk akcji", + "action-type":"Typ akcji", + "active":"Aktywny", + "add-notification-recipients-group":"Dodaj grupę odbiorców powiadomień", + "add-notification-template":"Dodaj szablon powiadomienia", + "add-recipient":"Dodaj odbiorcę", + "add-recipients":"Dodaj odbiorców", + "add-rule":"Dodaj regułę", + "add-stage":"Dodaj etap", + "add-template":"Dodaj szablon", + "after":"Po", + "alarm-assignment-trigger-settings":"Ustawienia wyzwalacza przypisania alarmu", + "alarm-comment-trigger-settings":"Ustawienia wyzwalacza komentarza alarmu", + "alarm-trigger-settings":"Ustawienia wyzwalacza alarmu", + "all":"Wszystkie", + "api-feature-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich funkcji API", + "api-usage-trigger-settings":"Ustawienia wyzwalacza korzystania z API", + "new-platform-version-trigger-settings":"Ustawienia wyzwalacza nowej wersji platformy", + "rate-limits-trigger-settings":"Ustawienia wyzwalacza przekroczenia limitów szybkości", + "at-least-one-should-be-selected":"Przynajmniej jedna pozycja musi być zaznaczona", + "basic-settings":"Podstawowe ustawienia", + "button-text":"Tekst przycisku", + "button-text-required":"Tekst przycisku jest wymagany", + "button-text-max-length":"Tekst przycisku powinien być krótszy lub równy {{ length }} znaków", + "compose":"Komponuj", + "conversation":"Rozmowa", + "conversation-required":"Rozmowa jest wymagana", + "copy-notification-template":"Skopiuj szablon powiadomienia", + "copy-rule":"Skopiuj regułę", + "copy-template":"Skopiuj szablon", + "create-new":"Utwórz nowe", + "created":"Utworzono", + "delete-notification-text":"Uważaj, po potwierdzeniu powiadomienie stanie się nieodwracalne.", + "delete-notification-title":"Czy na pewno chcesz usunąć powiadomienie?", + "delete-notifications-text":"Uważaj, po potwierdzeniu powiadomienia staną się nieodwracalne.", + "delete-notifications-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 powiadomienie} other {# powiadomienia} }?", + "delete-recipient-text":"Uważaj, po potwierdzeniu odbiorca stanie się nieodwracalny.", + "delete-recipient-title":"Czy na pewno chcesz usunąć odbiorcę '{{recipientName}}'?", + "delete-recipients-text":"Uważaj, po potwierdzeniu odbiorcy staną się nieodwracalni.", + "delete-recipients-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 odbiorcę} other {# odbiorców} }?", + "delete-request-text":"Uważaj, po potwierdzeniu żądanie stanie się nieodwracalne.", + "delete-request-title":"Czy na pewno chcesz usunąć żądanie?", + "delete-requests-text":"Uważaj, po potwierdzeniu żądania staną się nieodwracalne.", + "delete-requests-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 żądanie} other {# żądań} }?", + "delete-rule-text":"Uważaj, po potwierdzeniu reguła stanie się nieodwracalna.", + "delete-rule-title":"Czy na pewno chcesz usunąć regułę '{{ruleName}}'?", + "delete-rules-text":"Uważaj, po potwierdzeniu reguły staną się nieodwracalne.", + "delete-rules-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 regułę} other {# reguł} }?", + "delete-template-text":"Uważaj, po potwierdzeniu szablon stanie się nieodwracalny.", + "delete-template-title":"Czy na pewno chcesz usunąć szablon '{{templateName}}'?", + "delete-templates-text":"Uważaj, po potwierdzeniu szablony staną się nieodwracalne.", + "delete-templates-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 szablon} other {# szablonów} }?", + "deleted":"Usunięto", + "delivery-method":{ + "delivery-method":"Metoda dostarczania", + "email":"Email", + "email-preview":"Podgląd powiadomienia email", + "slack":"Slack", + "slack-preview":"Podgląd powiadomienia Slack", + "microsoft-teams":"Microsoft Teams", + "microsoft-teams-preview":"Podgląd powiadomienia Microsoft Teams", + "sms":"SMS", + "sms-preview":"Podgląd powiadomienia SMS", + "web":"Web", + "web-preview":"Podgląd powiadomienia web" + }, + "delivery-method-not-configure-click":"Metoda dostarczania nie jest skonfigurowana. Kliknij, aby skonfigurować.", + "delivery-method-not-configure-contact":"Metoda dostarczania nie jest skonfigurowana. Skontaktuj się z administratorem systemu.", + "delivery-methods":"Metody dostarczania", + "description":"Opis", + "device-activity-trigger-settings":"Ustawienia wyzwalacza aktywności urządzenia", + "device-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich urządzeń", + "device-profiles-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich profili urządzeń", + "disabled":"Wyłączone", + "edit-notification-recipients-group":"Edytuj grupę odbiorców powiadomień", + "edit-notification-template":"Edytuj szablon powiadomień", + "edit-rule":"Edytuj regułę", + "edit-template":"Edytuj szablon", + "enabled":"Włączone", + "entities-limit-trigger-settings":"Ustawienia wyzwalacza limitu encji", + "entity-action-trigger-settings":"Ustawienia wyzwalacza akcji encji", + "entity-type":"Typ encji", + "escalation-chain":"Łańcuch eskalacji", + "failed-send":"Błędy wysyłki", + "fails":"{ count, plural, =1 {1 błąd} other {# błędów} }", + "filter":"Filtr", + "first-recipient":"Pierwszy odbiorca", + "inactive":"Nieaktywne", + "inbox":"Skrzynka odbiorcza", + "notification-inbox":"Powiadomienia / Skrzynka odbiorcza", + "input-field-support-templatization":"Pole wejściowe obsługuje szablonowanie.", + "input-fields-support-templatization":"Pola wejściowe obsługują szablonowanie.", + "link":"Odnośnik", + "link-required":"Odnośnik jest wymagany", + "link-type":{ + "dashboard":"Otwórz pulpit nawigacyjny", + "link":"Otwórz link URL" + }, + "loading-notifications":"Ładowanie powiadomień...", + "management":"Zarządzanie powiadomieniami", + "mark-all-as-read":"Oznacz wszystkie jako przeczytane", + "mark-as-read":"Oznacz jako przeczytane", + "message":"Wiadomość", + "message-required":"Wiadomość jest wymagana", + "message-max-length":"Wiadomość powinna mieć mniej niż {{ length }} znaków", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana", + "new-notification":"Nowe powiadomienie", + "no-inbox-notification":"Nie znaleziono powiadomień", + "no-notification-request":"Brak zapytania o powiadomienie", + "no-notification-templates":"Nie znaleziono szablonów powiadomień", + "no-notifications-yet":"Brak powiadomień", + "no-recipients-notification":"Powiadomienie bez odbiorców", + "no-rule":"Nie skonfigurowano reguły", + "no-rules-notification":"Brak powiadomienia o regułach", + "no-severity-found":"Nie znaleziono powagi", + "no-severity-matching":"Nie znaleziono '{{severity}}'.", + "no-template-matching":"Nie znaleziono zasobów pasujących do '{{template}}'.", + "not-found-slack-recipient":"Nie znaleziono odbiorcy Slack", + "notification":"Powiadomienie", + "notification-center":"Centrum powiadomień", + "notify":"powiadomić", + "notify-again":"Powiadom ponownie", + "notify-alarm-action":{ + "acknowledged":"Alarm potwierdzony", + "assigned":"Alarm przypisany", + "cleared":"Alarm wyczyszczony", + "created":"Alarm utworzony", + "severity-changed":"Zmieniono powagę alarmu", + "unassigned":"Alarm nieprzypisany" + }, + "notify-on":"Powiadamiaj o", + "notify-on-comment-update":"Powiadamiaj o aktualizacji komentarza", + "notify-on-required":"Wymagane jest powiadomienie o", + "notify-on-unassign":"Powiadamiaj o cofnięciu przypisania", + "notify-only-user-comments":"Powiadamiaj tylko o komentarzach użytkownika", + "only-rule-chain-lifecycle-failures":"Tylko niepowodzenia cyklu życia łańcucha reguł", + "only-rule-node-lifecycle-failures":"Tylko niepowodzenia cyklu życia węzła reguły", + "platform-users":"Użytkownicy platformy", + "rate-limits":"Limity częstotliwości", + "rate-limits-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich limitów częstotliwości", + "recipient":"Odbiorca", + "recipient-group":"Grupa odbiorców", + "recipient-type":{ + "affected-tenant-administrators":"Dotknięci administratorzy najemcy", + "affected-user":"Dotknięty użytkownik", + "all-users":"Wszyscy użytkownicy", + "customer-users":"Użytkownicy klientów", + "system-administrators":"Administratorzy systemu", + "tenant-administrators":"Administratorzy najemcy", + "user-filters":"Filtr użytkowników", + "user-list":"Lista użytkowników", + "users-entity-owner":"Użytkownicy właściciela jednostki" + }, + "recipients":"Odbiorcy", + "notification-recipients":"Powiadomienia / Odbiorcy", + "recipients-count":"{ count, plural, =1 {1 odbiorca} other {# odbiorców} }", + "recipients-required":"Odbiorcy są wymagani", + "refresh-allow-delivery-method":"Odśwież metodę dostarczania", + "request-search":"Wyszukaj żądanie", + "request-status":{ + "processing":"Przetwarzanie", + "scheduled":"Zaplanowane", + "sent":"Wysłane" + }, + "review":"Recenzja", + "rule":"Reguła", + "rule-chain-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich łańcuchów reguł", + "rule-engine-events-trigger-settings":"Ustawienia wyzwalaczy zdarzeń silnika reguł", + "rule-engine-filter":"Filtr silnika reguł", + "rule-name":"Nazwa reguły", + "rule-name-required":"Nazwa jest wymagana", + "rule-disable":"Wyłącz regułę powiadomienia", + "rule-enable":"Włącz regułę powiadomienia", + "rule-node-filter":"Filtr węzła reguły", + "rules":"Reguły", + "notification-rules":"Powiadomienia / Reguły", + "scheduler-later":"Zaplanuj na później", + "search-notification":"Wyszukaj powiadomienia", + "search-recipients":"Wyszukaj odbiorców", + "search-rules":"Wyszukaj reguły", + "search-templates":"Wyszukaj szablony", + "see-documentation":"Zobacz dokumentację", + "selected-notifications":"{ count, plural, =1 {1 powiadomienie} other {# powiadomień} } wybrano", + "selected-recipients":"{ count, plural, =1 {1 odbiorca} other {# odbiorców} } wybrano", + "selected-requests":"{ count, plural, =1 {1 żądanie} other {# żądań} } wybrano", + "selected-rules":"{ count, plural, =1 {1 reguła} other {# reguł} } wybrano", + "selected-template":"{ count, plural, =1 {1 szablon} other {# szablonów} } wybrano", + "send-notification":"Wyślij powiadomienie", + "sent":"Wysłane", + "notification-sent":"Powiadomienia / Wysłane", + "set-entity-from-notification":"Ustaw jednostkę z powiadomienia na stan pulpitu nawigacyjnego", + "slack-chanel-type":"Typ kanału Slack", + "slack-chanel-types":{ + "direct":"Wiadomość bezpośrednia", + "private-channel":"Prywatny kanał", + "public-channel":"Publiczny kanał" + }, + "start-from-scratch":"Rozpocznij od początku", + "status":"Status", + "stop-escalation-alarm-status-become":"Zatrzymaj eskalację, gdy status alarmu stanie się:", + "subject":"Temat", + "subject-required":"Temat jest wymagany", + "template":"Szablon", + "template-name":"Nazwa szablonu", + "template-required":"Szablon jest wymagany", + "template-type":{ + "alarm":"Alarm", + "alarm-assignment":"Przypisanie alarmu", + "alarm-comment":"Komentarz do alarmu", + "api-usage-limit":"Limit użycia API", + "device-activity":"Aktywność urządzenia", + "entities-limit":"Limit jednostek", + "entity-action":"Akcja jednostki", + "general":"Ogólny", + "rule-engine-lifecycle-event":"Zdarzenie cyklu życia silnika reguł", + "rule-node":"Węzeł reguły", + "new-platform-version":"Nowa wersja platformy", + "rate-limits":"Przekroczono limity częstotliwości" + }, + "templates":"Szablony", + "notification-templates":"Powiadomienia / Szablony", + "tenant-profiles-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich profili najemców", + "tenants-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich najemców", + "threshold":"Próg", + "theme-color":"Kolor motywu", + "time":"Czas", + "track-rule-node-events":"Śledź zdarzenia węzła reguły", + "trigger":{ + "alarm":"Alarm", + "alarm-assignment":"Przypisanie alarmu", + "alarm-comment":"Komentarz do alarmu", + "api-usage-limit":"Limit użycia API", + "device-activity":"Aktywność urządzenia", + "entities-limit":"Limit jednostek", + "entity-action":"Akcja jednostki", + "rule-engine-lifecycle-event":"Zdarzenie cyklu życia silnika reguł", + "new-platform-version":"Nowa wersja platformy", + "rate-limits":"Przekroczono limity częstotliwości", + "trigger":"Wyzwalacz", + "trigger-required":"Wymagany jest wyzwalacz" + }, + "type":"Typ", + "unread":"Nieprzeczytane", + "updated":"Zaktualizowane", + "use-template":"Użyj szablonu", + "view-all":"Pokaż wszystko", + "warning":"Ostrzeżenie", + "webhook-url":"URL Webhooka", + "webhook-url-required":"URL Webhooka jest wymagany", + "channel-name":"Nazwa kanału", + "channel-name-required":"Nazwa kanału jest wymagana", + "settings":{ + "notification-settings":"Ustawienia powiadomień", + "reset-all":"Zresetuj wszystkie ustawienia", + "reset-all-title":"Czy na pewno chcesz zresetować formularz?", + "reset-all-text":"Po potwierdzeniu, formularz ustawień zostanie zresetowany do domyślnej wartości i zapisany.", + "type":"Typ", + "enable-all":"Włącz wszystkie", + "disable-all":"Wyłącz wszystkie", + "delivery-not-configured":"Metoda dostarczania nie jest skonfigurowana" + } + }, + "ota-update":{ + "add":"Dodaj pakiet", + "assign-firmware":"Przypisane oprogramowanie", + "assign-firmware-required":"Przypisane oprogramowanie jest wymagane", + "assign-software":"Przypisane oprogramowanie", + "assign-software-required":"Przypisane oprogramowanie jest wymagane", + "auto-generate-checksum":"Automatycznie generuj sumę kontrolną", + "checksum":"Suma kontrolna", + "checksum-hint":"Jeśli suma kontrolna jest pusta, zostanie wygenerowana automatycznie", + "checksum-algorithm":"Algorytm sumy kontrolnej", + "checksum-copied-message":"Suma kontrolna pakietu została skopiowana do schowka", + "change-firmware":"Zmiana oprogramowania może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", + "change-software":"Zmiana oprogramowania może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", + "chose-compatible-device-profile":"Wgrany pakiet będzie dostępny tylko dla urządzeń z wybranym profilem.", + "chose-firmware-distributed-device":"Wybierz oprogramowanie, które zostanie rozprowadzone na urządzenia", + "chose-software-distributed-device":"Wybierz oprogramowanie, które zostanie rozprowadzone na urządzenia", + "content-type":"Typ zawartości", + "copy-checksum":"Kopiuj sumę kontrolną", + "copy-direct-url":"Kopiuj bezpośredni adres URL", + "copyId":"Kopiuj identyfikator pakietu", + "copied":"Skopiowane!", + "delete":"Usuń pakiet", + "delete-ota-update-text":"Bądź ostrożny, po potwierdzeniu aktualizacja OTA stanie się nieodwracalna.", + "delete-ota-update-title":"Czy na pewno chcesz usunąć aktualizację OTA „{{title}}”?", + "delete-ota-updates-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane aktualizacje OTA zostaną usunięte.", + "delete-ota-updates-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 aktualizację OTA} other {# aktualizacji OTA} }?", + "description":"Opis", + "direct-url":"Bezpośredni adres URL", + "direct-url-copied-message":"Bezpośredni adres URL pakietu został skopiowany do schowka", + "direct-url-required":"Bezpośredni adres URL jest wymagany", + "download":"Pobierz pakiet", + "drop-file":"Przeciągnij i upuść plik pakietu lub kliknij, aby wybrać plik do przesłania.", + "drop-package-file-or":"Przeciągnij i upuść plik pakietu lub", + "file-name":"Nazwa pliku", + "file-size":"Rozmiar pliku", + "file-size-bytes":"Rozmiar pliku w bajtach", + "idCopiedMessage":"Identyfikator pakietu został skopiowany do schowka", + "no-firmware-matching":"Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania układowego pasujących do „{{entity}}”.", + "no-firmware-text":"Brak kompatybilnych pakietów aktualizacji oprogramowania układowego.", + "no-packages-text":"Nie znaleziono pakietów", + "no-software-matching":"Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania pasujących do „{{entity}}”.", + "no-software-text":"Brak kompatybilnych pakietów aktualizacji oprogramowania.", + "ota-update":"Aktualizacja OTA", + "ota-update-details":"Szczegóły aktualizacji OTA", + "ota-updates":"Aktualizacje OTA", + "package-file":"Plik pakietu", + "package-type":"Typ pakietu", + "packages-repository":"Repozytorium pakietów", + "search":"Szukaj pakietów", + "selected-package":"{ count, plural, =1 {1 pakiet} other {# pakietów} } wybrano", + "title":"Tytuł", + "title-required":"Tytuł jest wymagany.", + "title-max-length":"Tytuł powinien mieć mniej niż 256", + "types":{ + "firmware":"Oprogramowanie układowe", + "software":"Oprogramowanie" + }, + "upload-binary-file":"Prześlij plik binarny", + "use-external-url":"Użyj zewnętrznego adresu URL", + "version":"Wersja", + "version-required":"Wersja jest wymagana.", + "version-tag":"Tag wersji", + "version-tag-hint":"Niestandardowy tag powinien pasować do wersji pakietu zgłoszonej przez Twoje urządzenie.", + "version-max-length":"Wersja powinna mieć mniej niż 256", + "warning-after-save-no-edit":"Po przesłaniu pakietu nie będziesz mógł edytować tytułu, wersji, profilu urządzenia i typu pakietu." + }, + "position":{ + "top":"Góra", + "bottom":"Dół", + "left":"Lewo", + "right":"Prawo" + }, + "profile":{ + "profile":"Profil", + "last-login-time":"Ostatnie logowanie", + "change-password":"Zmień hasło", + "current-password":"Obecne hasło", + "copy-jwt-token":"Kopiuj token JWT", + "jwt-token":"Token JWT", + "token-valid-till":"Token jest ważny do", + "tokenCopiedSuccessMessage":"Token JWT został skopiowany do schowka", + "tokenCopiedWarnMessage":"Token JWT wygasł! Proszę odświeżyć stronę." + }, + "profiles":{ + "profiles":"Profile" + }, + "security":{ + "security":"Bezpieczeństwo", + "general-settings":"Ogólne ustawienia bezpieczeństwa", + "access-token":"Token dostępu", + "access-token-required":"Token dostępu jest wymagany", + "clientId":"ID klienta", + "clientId-required":"ID klienta jest wymagane", + "username":"Nazwa użytkownika", + "username-required":"Nazwa użytkownika jest wymagana", + "ca-cert":"Certyfikat CA", + "2fa":{ + "2fa":"Uwierzytelnianie dwuskładnikowe", + "2fa-description":"Uwierzytelnianie dwuskładnikowe chroni Twoje konto przed nieautoryzowanym dostępem. Wystarczy wprowadzić kod zabezpieczeń podczas logowania.", + "authenticate-with":"Możesz uwierzytelniać się za pomocą:", + "disable-2fa-provider-text":"Wyłączenie {{name}} spowoduje obniżenie poziomu bezpieczeństwa Twojego konta", + "disable-2fa-provider-title":"Czy na pewno chcesz wyłączyć {{name}}?", + "get-new-code":"Pobierz nowy kod", + "main-2fa-method":"Użyj jako główna metoda uwierzytelniania dwuskładnikowego", + "dialog":{ + "activation-step-description-email":"Następnym razem, gdy się zalogujesz, zostaniesz poproszony o wprowadzenie kodu zabezpieczeń, który zostanie wysłany na Twój adres e-mail.", + "activation-step-description-sms":"Następnym razem, gdy się zalogujesz, zostaniesz poproszony o wprowadzenie kodu zabezpieczeń, który zostanie wysłany na numer telefonu.", + "activation-step-description-totp":"Następnym razem, gdy się zalogujesz, będziesz musiał wprowadzić kod uwierzytelniania dwuskładnikowego.", + "activation-step-label":"Aktywacja", + "backup-code-description":"Wydrukuj kody, aby mieć je pod ręką, gdy będziesz musiał ich użyć do zalogowania się na swoje konto. Każdy kod zapasowy można użyć tylko raz.", + "backup-code-warn":"Po opuszczeniu tej strony te kody nie będą już dostępne. Przechowuj je bezpiecznie, korzystając z dostępnych opcji poniżej.", + "download-txt":"Pobierz (txt)", + "email-step-description":"Podaj adres e-mail, który będziesz używać jako autentykatora.", + "email-step-label":"E-mail", + "enable-email-title":"Włącz autentykator e-mail", + "enable-sms-title":"Włącz autentykator SMS", + "enable-totp-title":"Włącz autentykator aplikacji", + "enter-verification-code":"Wprowadź tutaj 6-cyfrowy kod", + "get-backup-code-title":"Pobierz kod zapasowy", + "next":"Dalej", + "scan-qr-code":"Zeskanuj ten kod QR za pomocą swojej aplikacji uwierzytelniającej", + "send-code":"Wyślij kod", + "sms-step-description":"Podaj numer telefonu, który będziesz używać jako autentykatora.", + "sms-step-label":"Numer telefonu", + "success":"Sukces!", + "totp-step-description-install":"Możesz zainstalować aplikacje takie jak Google Authenticator, Authy lub Duo.", + "totp-step-description-open":"Otwórz aplikację autentykatora na swoim telefonie komórkowym.", + "totp-step-label":"Pobierz aplikację", + "verification-code":"Kod weryfikacyjny 6-cyfrowy", + "verification-code-invalid":"Nieprawidłowy format kodu weryfikacyjnego", + "verification-code-incorrect":"Kod weryfikacyjny jest niepoprawny", + "verification-code-many-request":"Zbyt wiele żądań sprawdzenia kodu weryfikacyjnego", + "verification-step-description":"Wprowadź 6-cyfrowy kod, który właśnie wysłaliśmy na adres {{address}}", + "verification-step-label":"Weryfikacja" + }, + "provider":{ + "email":"E-mail", + "email-description":"Użyj kodu zabezpieczeń wysłanego na Twój adres e-mail do uwierzytelniania.", + "email-hint":"Kody uwierzytelniania są wysyłane przez e-mail na adres {{ info }}", + "sms":"SMS", + "sms-description":"Użyj telefonu do uwierzytelniania. Wyślemy Ci kod zabezpieczeń za pomocą wiadomości SMS podczas logowania.", + "sms-hint":"Kody uwierzytelniania są wysyłane jako wiadomości tekstowe na numer telefonu {{ info }}", + "totp":"Aplikacja autentykatora", + "totp-description":"Używaj aplikacji takich jak Google Authenticator, Authy lub Duo na swoim telefonie do uwierzytelniania. Wygeneruje ona kod zabezpieczeń do logowania.", + "totp-hint":"Aplikacja autentykatora jest skonfigurowana dla Twojego konta", + "backup_code":"Kod zapasowy", + "backup-code-description":"Te jednorazowe kody do wydruku pozwalają Ci się zalogować, gdy jesteś z dala od telefonu, na przykład podczas podróży.", + "backup-code-hint":"Obecnie aktywnych jest {{ info }} jednorazowych kodów" + } + }, + "password-requirement":{ + "at-least":"Przynajmniej:", + "character":"{ count, plural, =1 {1 znak} other {# znaków} }", + "digit":"{ count, plural, =1 {1 cyfra} other {# cyfr} }", + "incorrect-password-try-again":"Nieprawidłowe hasło. Spróbuj ponownie", + "lowercase-letter":"{ count, plural, =1 {1 mała litera} other {# małe litery} }", + "new-passwords-not-match":"Nowe hasło nie pasuje", + "password-should-not-contain-spaces":"Twoje hasło nie powinno zawierać spacji", + "password-not-meet-requirements":"Hasło nie spełnia wymagań", + "password-requirements":"Wymagania dotyczące hasła", + "password-should-difference":"Nowe hasło powinno różnić się od bieżącego", + "special-character":"{ count, plural, =1 {1 znak specjalny} other {# znaków specjalnych} }", + "uppercase-letter":"{ count, plural, =1 {1 wielka litera} other {# wielkie litery} }", + "at-most":"Maksymalnie:" + } + }, + "relation":{ + "relations":"Relacje", + "direction":"Kierunek", + "clear-relation-type":"Wyczyść typ relacji", + "search-direction":{ + "FROM":"Od", + "TO":"Do" + }, + "direction-type":{ + "FROM":"from", + "TO":"to" + }, + "from-relations":"Wychodzące relacje", + "to-relations":"Przychodzące relacje", + "selected-relations":"{ count, plural, =1 {1 relacja} other {# relacji} } wybrano", + "type":"Typ", + "to-entity-type":"Typ jednostki docelowej", + "to-entity-name":"Nazwa jednostki docelowej", + "from-entity-type":"Typ jednostki źródłowej", + "from-entity-name":"Nazwa jednostki źródłowej", + "to-entity":"Jednostka docelowa", + "from-entity":"Jednostka źródłowa", + "delete":"Usuń relację", + "relation-type":"Typ relacji", + "relation-type-required":"Wymagany jest typ relacji.", + "relation-type-max-length":"Typ relacji powinien mieć mniej niż 256 znaków", + "any-relation-type":"Dowolny typ", + "add":"Dodaj relację", + "edit":"Edytuj relację", + "delete-to-relation-title":"Czy na pewno chcesz usunąć relację do jednostki '{{entityName}}'?", + "delete-to-relation-text":"Bądź ostrożny, po potwierdzeniu jednostka '{{entityName}}' zostanie odłączona od bieżącej jednostki.", + "delete-to-relations-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 relacja} other {# relacje} }?", + "delete-to-relations-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a odpowiednie jednostki zostaną odłączone od bieżącej jednostki.", + "delete-from-relation-title":"Czy na pewno chcesz usunąć relację z jednostki '{{entityName}}'?", + "delete-from-relation-text":"Bądź ostrożny, po potwierdzeniu bieżąca jednostka zostanie odłączona od jednostki '{{entityName}}'.", + "delete-from-relations-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 relacja} other {# relacje} }?", + "delete-from-relations-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a bieżąca jednostka zostanie odłączona od odpowiednich jednostek.", + "remove-relation-filter":"Usuń filtr relacji", + "remove-filter":"Usuń filtr", + "add-relation-filter":"Dodaj filtr relacji", + "any-relation":"Dowolna relacja", + "relation-filters":"Filtry relacji", + "additional-info":"Dodatkowe informacje (JSON)", + "invalid-additional-info":"Nie można sparsować dodatkowych informacji json.", + "no-relations-text":"Nie znaleziono relacji" + }, + "resource":{ + "add":"Dodaj zasób", + "all-types":"Wszystkie", + "copyId":"Skopiuj identyfikator zasobu", + "delete":"Usuń zasób", + "delete-resource-text":"Bądź ostrożny, po potwierdzeniu zasób zostanie bezpowrotnie usunięty.", + "delete-resource-title":"Czy na pewno chcesz usunąć zasób '{{resourceTitle}}'?", + "delete-resources-action-title":"Usuń { count, plural, =1 {1 zasób} other {# zasobów} }", + "delete-resources-text":"Proszę zauważyć, że wybrane zasoby, nawet jeśli są używane w profilach urządzeń, zostaną usunięte.", + "delete-resources-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 zasób} other {# zasobów} }?", + "download":"Pobierz zasób", + "drop-file":"Upuść plik zasobu lub kliknij, aby wybrać plik do przesłania.", + "drop-resource-file-or":"Przeciągnij i upuść plik zasobu lub", + "empty":"Zasób jest pusty", + "file-name":"Nazwa pliku", + "idCopiedMessage":"Identyfikator zasobu został skopiowany do schowka", + "no-resource-matching":"Nie znaleziono zasobów pasujących do '{{widgetsBundle}}'.", + "no-resource-text":"Nie znaleziono zasobów", + "open-widgets-bundle":"Otwórz paczkę widżetów", + "resource":"Zasób", + "resource-file":"Plik zasobu", + "resource-files":"Pliki zasobów", + "resource-library-details":"Szczegóły zasobu", + "resource-type":"Typ zasobu", + "resources-library":"Biblioteka zasobów", + "search":"Szukaj zasobów", + "selected-resources":"{ count, plural, =1 {1 zasób} other {# zasobów} } wybrano", + "system":"System", + "title":"Tytuł", + "title-required":"Tytuł jest wymagany.", + "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", + "type":{ + "jks":"JKS", + "js-module":"Moduł JS", + "lwm2m-model":"Model LWM2M", + "pkcs-12":"PKCS #12" + } + }, + "rulechain":{ + "rulechain":"Łańcuch reguł", + "rulechain-events":"Zdarzenia łańcucha reguł", + "rulechains":"Łańcuchy reguł", + "root":"Korzeń", + "delete":"Usuń łańcuch reguł", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", + "description":"Opis", + "add":"Dodaj łańcuch reguł", + "set-root":"Ustaw łańcuch reguł jako korzeń", + "set-root-rulechain-title":"Jesteś pewien, że chcesz ustawić łańcuch reguł '{{ruleChainName}}' jako korzeń?", + "set-root-rulechain-text":"Po potwierdzeniu łańcuch reguł stanie się korzeniem i będzie obsługiwał wszystkie przychodzące wiadomości transportowe.", + "delete-rulechain-title":"Jesteś pewien, że chcesz usunąć łańcuch reguł '{{ruleChainName}}'?", + "delete-rulechain-text":"Bądź ostrożny, po potwierdzeniu łańcuch reguł i wszystkie powiązane dane staną się nieodwracalne.", + "delete-rulechains-title":"Jesteś pewien, że chcesz usunąć { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }?", + "delete-rulechains-action-title":"Usuń { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }", + "delete-rulechains-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", + "add-rulechain-text":"Dodaj nowy łańcuch reguł", + "no-rulechains-text":"Nie znaleziono łańcuchów reguł", + "rulechain-details":"Szczegóły łańcucha reguł", + "details":"Szczegóły", + "events":"Zdarzenia", + "system":"System", + "import":"Importuj łańcuch reguł", + "export":"Eksportuj łańcuch reguł", + "export-failed-error":"Nie można wyeksportować łańcucha reguł: {{error}}", + "create-new-rulechain":"Utwórz nowy łańcuch reguł", + "rulechain-file":"Plik łańcucha reguł", + "invalid-rulechain-file-error":"Nie można zaimportować łańcucha reguł: Nieprawidłowa struktura danych łańcucha reguł.", + "copyId":"Skopiuj identyfikator łańcucha reguł", + "idCopiedMessage":"Identyfikator łańcucha reguł został skopiowany do schowka", + "select-rulechain":"Wybierz łańcuch reguł", + "no-rulechains-matching":"Nie znaleziono łańcuchów reguł pasujących do '{{entity}}'.", + "rulechain-required":"Łańcuch reguł jest wymagany", + "management":"Zarządzanie regułami", + "debug-mode":"Tryb debugowania", + "search":"Wyszukaj łańcuchy reguł", + "selected-rulechains":"{ count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} } wybranych", + "open-rulechain":"Otwórz łańcuch reguł", + "edge-template-root":"Korzeń szablonu", + "assign-to-edge":"Przypisz do krawędzi", + "edge-rulechain":"Łańcuch reguł krawędzi", + "unassign-rulechain-from-edge-text":"Po potwierdzeniu łańcuch reguł zostanie odłączony i nie będzie dostępny dla krawędzi.", + "unassign-rulechains-from-edge-title":"Jesteś pewien, że chcesz odłączyć { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }?", + "unassign-rulechains-from-edge-text":"Po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną odłączone i nie będą dostępne dla krawędzi.", + "assign-rulechain-to-edge-title":"Przypisz łańcuch(y) reguł do krawędzi", + "assign-rulechain-to-edge-text":"Wybierz łańcuchy reguł do przypisania do krawędzi", + "set-edge-template-root-rulechain":"Ustaw łańcuch reguł jako korzeń szablonu krawędzi", + "set-edge-template-root-rulechain-title":"Jesteś pewien, że chcesz ustawić łańcuch reguł '{{ruleChainName}}' jako korzeń szablonu krawędzi?", + "set-edge-template-root-rulechain-text":"Po potwierdzeniu łańcuch reguł stanie się korzeniem szablonu krawędzi i będzie korzeniem łańcucha reguł dla nowo tworzonych krawędzi.", + "invalid-rulechain-type-error":"Nie można zaimportować łańcucha reguł: Nieprawidłowy typ łańcucha reguł. Oczekiwany typ to {{expectedRuleChainType}}.", + "set-auto-assign-to-edge":"Przypisz łańcuch reguł do krawędzi podczas tworzenia", + "set-auto-assign-to-edge-title":"Jesteś pewien, że chcesz przypisać łańcuch reguł krawędzi '{{ruleChainName}}' do krawędzi podczas tworzenia?", + "set-auto-assign-to-edge-text":"Po potwierdzeniu łańcuch reguł krawędzi zostanie automatycznie przypisany do krawędzi podczas tworzenia.", + "unset-auto-assign-to-edge":"Nie przypisuj łańcucha reguł do krawędzi podczas tworzenia", + "unset-auto-assign-to-edge-title":"Jesteś pewien, że nie chcesz przypisywać łańcucha reguł krawędzi '{{ruleChainName}}' do krawędzi podczas tworzenia?", + "unset-auto-assign-to-edge-text":"Po potwierdzeniu łańcuch reguł krawędzi nie będzie automatycznie przypisywany do krawędzi podczas tworzenia.", + "unassign-rulechain-title":"Jesteś pewien, że chcesz odłączyć łańcuch reguł '{{ruleChainName}}'?", + "unassign-rulechains":"Odłącz łańcuchy reguł" + }, + "rulenode":{ + "rule-node-events":"Zdarzenia węzła reguły", + "details":"Szczegóły", + "events":"Zdarzenia", + "search":"Wyszukaj węzły", + "open-node-library":"Otwórz bibliotekę węzłów", + "close-node-library":"Zamknij bibliotekę węzłów", + "add":"Dodaj węzeł reguły", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", + "type":"Typ", + "rule-node-description":"Opis węzła reguły", + "delete":"Usuń węzeł reguły", + "select-all-objects":"Zaznacz wszystkie węzły i połączenia", + "deselect-all-objects":"Odznacz wszystkie węzły i połączenia", + "delete-selected-objects":"Usuń wybrane węzły i połączenia", + "delete-selected":"Usuń zaznaczone", + "create-nested-rulechain":"Utwórz zagnieżdżony łańcuch reguł", + "select-all":"Zaznacz wszystkie", + "copy-selected":"Skopiuj zaznaczone", + "deselect-all":"Odznacz wszystkie", + "rulenode-details":"Szczegóły węzła reguły", + "debug-mode":"Tryb debugowania", + "singleton-mode":"Tryb pojedynczej instancji", + "configuration":"Konfiguracja", + "link":"Połączenie", + "link-details":"Szczegóły połączenia węzła reguły", + "add-link":"Dodaj połączenie", + "link-label":"Etykieta połączenia", + "link-label-required":"Etykieta połączenia jest wymagana.", + "custom-link-label":"Niestandardowa etykieta połączenia", + "custom-link-label-required":"Niestandardowa etykieta połączenia jest wymagana.", + "link-labels":"Etykiety połączeń", + "link-labels-required":"Wymagane są etykiety połączeń.", + "no-link-labels-found":"Nie znaleziono etykiet połączeń", + "no-link-label-matching":"'{{label}}' nie zostało znalezione.", + "create-new-link-label":"Utwórz nową!", + "type-filter":"Filtr", + "type-filter-details":"Filtruj przychodzące wiadomości z określonymi warunkami", + "type-enrichment":"Wzbogacenie", + "type-enrichment-details":"Dodaj dodatkowe informacje do metadanych wiadomości", + "type-transformation":"Transformacja", + "type-transformation-details":"Zmień ładunek wiadomości i metadane", + "type-action":"Akcja", + "type-action-details":"Wykonaj specjalną akcję", + "type-external":"Zewnętrzne", + "type-external-details":"Komunikuje się z zewnętrznym systemem", + "type-rule-chain":"Łańcuch reguł", + "type-rule-chain-details":"Przekazuje przychodzące wiadomości do określonego łańcucha reguł", + "type-flow":"Przepływ", + "type-flow-details":"Organizuje przepływ wiadomości", + "type-input":"Wejście", + "type-input-details":"Logiczne wejście łańcucha reguł, przekazuje przychodzące wiadomości do następnego powiązanego węzła reguły", + "type-unknown":"Nieznany", + "type-unknown-details":"Nierozwiązany węzeł reguły", + "directive-is-not-loaded":"Zdefiniowana dyrektywa konfiguracji '{{directiveName}}' nie jest dostępna.", + "ui-resources-load-error":"Nie udało się załadować zasobów interfejsu użytkownika konfiguracji.", + "invalid-target-rulechain":"Nie można rozpoznać docelowego łańcucha reguł!", + "test-script-function":"Funkcja skryptu testowego", + "script-lang-java-script":"JavaScript", + "script-lang-tbel":"TBEL", + "message":"Wiadomość", + "message-type":"Typ wiadomości", + "select-message-type":"Wybierz typ wiadomości", + "message-type-required":"Wymagany jest typ wiadomości", + "metadata":"Metadane", + "metadata-required":"Wpisy metadanych nie mogą być puste.", + "output":"Wynik", + "test":"Test", + "help":"Pomoc", + "reset-debug-mode":"Zresetuj tryb debugowania we wszystkich węzłach", + "test-with-this-message":"{{test}} z tą wiadomością", + "queue-hint":"Wybierz kolejkę do przekazywania wiadomości do innej kolejki. Domyślnie używana jest kolejka „Main”.", + "queue-singleton-hint":"Wybierz kolejkę do przekazywania wiadomości w środowiskach wielu instancji. Domyślnie używana jest kolejka „Main”." + }, + "timezone":{ + "timezone":"Strefa czasowa", + "select-timezone":"Wybierz strefę czasową", + "no-timezones-matching":"Nie znaleziono stref czasowych pasujących do '{{timezone}}'.", + "timezone-required":"Strefa czasowa jest wymagana.", + "browser-time":"Czas przeglądarki" + }, + "queue":{ + "queue-name":"Kolejka", + "no-queues-found":"Nie znaleziono kolejek.", + "no-queues-matching":"Nie znaleziono kolejek pasujących do '{{queue}}'.", + "select-name":"Wybierz nazwę kolejki", + "name":"Nazwa", + "name-required":"Nazwa kolejki jest wymagana!", + "name-unique":"Nazwa kolejki nie jest unikalna!", + "name-pattern":"Nazwa kolejki zawiera znak inny niż alfanumeryczny ASCII, '.', '_', i '-'!", + "queue-required":"Kolejka jest wymagana!", + "topic-required":"Temat kolejki jest wymagany!", + "poll-interval-required":"Interwał odpytywania jest wymagany!", + "poll-interval-min-value":"Wartość interwału odpytywania nie może być mniejsza niż 1", + "partitions-required":"Partycje są wymagane!", + "partitions-min-value":"Wartość partycji nie może być mniejsza niż 1", + "pack-processing-timeout-required":"Limit czasu przetwarzania jest wymagany", + "pack-processing-timeout-min-value":"Wartość limitu czasu przetwarzania nie może być mniejsza niż 1", + "batch-size-required":"Rozmiar partii jest wymagany!", + "batch-size-min-value":"Wartość rozmiaru partii nie może być mniejsza niż 1", + "retries-required":"Liczba prób jest wymagana!", + "retries-min-value":"Wartość liczby prób nie może być ujemna", + "failure-percentage-required":"Procent awaryjnych wiadomości jest wymagany!", + "failure-percentage-min-value":"Wartość procenta awaryjnych wiadomości nie może być mniejsza niż 0", + "failure-percentage-max-value":"Wartość procenta awaryjnych wiadomości nie może być większa niż 100", + "pause-between-retries-required":"Odstęp między próbami jest wymagany!", + "pause-between-retries-min-value":"Wartość odstępu między próbami nie może być mniejsza niż 1", + "max-pause-between-retries-required":"Maksymalny odstęp między próbami jest wymagany!", + "max-pause-between-retries-min-value":"Wartość maksymalnego odstępu między próbami nie może być mniejsza niż 1", + "submit-strategy-type-required":"Typ strategii przesyłania jest wymagany!", + "processing-strategy-type-required":"Typ strategii przetwarzania jest wymagany!", + "queues":"Kolejki", + "selected-queues":"{ count, plural, =1 {1 kolejka} other {# kolejek} } wybranych", + "delete-queue-title":"Czy na pewno chcesz usunąć kolejkę '{{queueName}}'?", + "delete-queues-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 kolejkę} other {# kolejki} }?", + "delete-queue-text":"Bądź ostrożny, po potwierdzeniu kolejka i wszystkie związane z nią dane zostaną bezpowrotnie usunięte.", + "delete-queues-text":"Po potwierdzeniu wszystkie wybrane kolejki zostaną usunięte i nie będą dostępne.", + "search":"Wyszukaj kolejkę", + "add":"Dodaj kolejkę", + "details":"Szczegóły kolejki", + "topic":"Temat", + "submit-settings":"Ustawienia przesyłania", + "submit-strategy":"Typ strategii *", + "grouping-parameter":"Parametr grupowania", + "processing-settings":"Ustawienia przetwarzania", + "processing-strategy":"Typ przetwarzania *", + "retries-settings":"Ustawienia prób", + "polling-settings":"Ustawienia odpytywania", + "batch-processing":"Przetwarzanie partiami", + "poll-interval":"Interwał odpytywania", + "partitions":"Partycje", + "immediate-processing":"Natychmiastowe przetwarzanie", + "consumer-per-partition":"Wysyłaj odpytywanie dla każdego konsumenta", + "consumer-per-partition-hint":"Włącz oddzielnych konsumentów dla każdej partycji", + "processing-timeout":"Przetwarzaj w ciągu, ms", + "batch-size":"Rozmiar partii", + "retries":"Liczba prób (0 - nieograniczona)", + "failure-percentage":"Procent awaryjnych wiadomości do pominięcia prób", + "pause-between-retries":"Odstęp między próbami, s", + "max-pause-between-retries":"Maksymalny odstęp między próbami, s", + "delete":"Usuń kolejkę", + "copyId":"Skopiuj Id kolejki", + "idCopiedMessage":"Id kolejki zostało skopiowane do schowka", + "description":"Opis", + "description-hint":"Ten tekst będzie wyświetlany w opisie kolejki zamiast wybranej strategii", + "alt-description":"Strategia przesyłania: {{submitStrategy}}, Strategia przetwarzania: {{processingStrategy}}", + "custom-properties":"Własne właściwości", + "custom-properties-hint":"Własne właściwości tworzenia kolejki (tematu), np. 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies":{ + "sequential-by-originator-label":"Sekwencyjne według nadawcy", + "sequential-by-originator-hint":"Nowa wiadomość, na przykład dla urządzenia A, nie jest przesyłana, dopóki poprzednia wiadomość dla urządzenia A nie zostanie potwierdzona", + "sequential-by-tenant-label":"Sekwencyjne według najemcy", + "sequential-by-tenant-hint":"Nowa wiadomość, na przykład dla najemcy A, nie jest przesyłana, dopóki poprzednia wiadomość dla najemcy A nie zostanie potwierdzona", + "sequential-label":"Sekwencyjne", + "sequential-hint":"Nowa wiadomość nie jest przesyłana, dopóki poprzednia wiadomość nie zostanie potwierdzona", + "burst-label":"Burst", + "burst-hint":"Wszystkie wiadomości są przesyłane do łańcuchów reguł w kolejności ich przyjścia", + "batch-label":"Partia", + "batch-hint":"Nowa partia nie jest przesyłana, dopóki poprzednia partia nie zostanie potwierdzona", + "skip-all-failures-label":"Pomiń wszystkie awarie", + "skip-all-failures-hint":"Ignoruj wszystkie awarie", + "skip-all-failures-and-timeouts-label":"Pomiń wszystkie awarie i przerwy czasowe", + "skip-all-failures-and-timeouts-hint":"Ignoruj wszystkie awarie i przerwy czasowe", + "retry-all-label":"Ponów wszystkie", + "retry-all-hint":"Ponów wszystkie wiadomości z pakietu przetwarzania", + "retry-failed-label":"Ponów nieudane", + "retry-failed-hint":"Ponów wszystkie nieudane wiadomości z pakietu przetwarzania", + "retry-timeout-label":"Ponów przerwę czasową", + "retry-timeout-hint":"Ponów wszystkie wiadomości przekroczone czasem z pakietu przetwarzania", + "retry-failed-and-timeout-label":"Ponów nieudane i przerwę czasową", + "retry-failed-and-timeout-hint":"Ponów wszystkie nieudane i przekroczone czasem wiadomości z pakietu przetwarzania" + } + }, + "server-error":{ + "general":"Ogólny błąd serwera", + "authentication":"Błąd uwierzytelniania", + "jwt-token-expired":"Token JWT wygasł", + "tenant-trial-expired":"Próba najemcy wygasła", + "credentials-expired":"Wygasły poświadczenia", + "permission-denied":"Brak uprawnień", + "invalid-arguments":"Nieprawidłowe argumenty", + "bad-request-params":"Nieprawidłowe parametry żądania", + "item-not-found":"Nie znaleziono elementu", + "too-many-requests":"Zbyt wiele żądań", + "too-many-updates":"Zbyt wiele aktualizacji" + }, + "tenant":{ + "tenant":"Najemca", + "tenants":"Najemcy", + "management":"Zarządzanie najemcami", + "add":"Dodaj najemcę", + "admins":"Administratorzy", + "manage-tenant-admins":"Zarządzaj administratorami najemcy", + "delete":"Usuń najemcę", + "add-tenant-text":"Dodaj nowego najemcę", + "no-tenants-text":"Nie znaleziono najemców", + "tenant-details":"Szczegóły najemcy", + "title-max-length":"Tytuł powinien zawierać mniej niż 256 znaków", + "delete-tenant-title":"Czy na pewno chcesz usunąć najemcę '{{tenantTitle}}'?", + "delete-tenant-text":"Bądź ostrożny, po potwierdzeniu najemca i wszystkie związane z nim dane zostaną bezpowrotnie usunięte.", + "delete-tenants-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 najemcę} other {# najemców} }?", + "delete-tenants-action-title":"Usuń { count, plural, =1 {1 najemcę} other {# najemców} }", + "delete-tenants-text":"Bądź ostrożny, po potwierdzeniu wszyscy wybrani najemcy zostaną usunięci, a wszystkie związane z nimi dane zostaną bezpowrotnie usunięte.", + "title":"Tytuł", + "title-required":"Tytuł jest wymagany.", + "description":"Opis", + "details":"Szczegóły", + "events":"Zdarzenia", + "copyId":"Kopiuj Id najemcy", + "idCopiedMessage":"Id najemcy zostało skopiowane do schowka", + "select-tenant":"Wybierz najemcę", + "no-tenants-matching":"Nie znaleziono najemców pasujących do '{{entity}}'", + "tenant-required":"Najemca jest wymagany", + "search":"Szukaj najemców", + "selected-tenants":"{ count, plural, =1 {1 najemca} other {# najemcy} } wybrano", + "isolated-tb-rule-engine":"Użyj izolowanych kolejek ThingsBoard Rule Engine", + "isolated-tb-rule-engine-details":"Każdy najemca będzie miał dedykowane kolejki Rule Engine" + }, + "tenant-profile":{ + "tenant-profile":"Profil najemcy", + "tenant-profiles":"Profile najemców", + "add":"Dodaj profil najemcy", + "add-profile":"Dodaj profil", + "edit":"Edytuj profil najemcy", + "tenant-profile-details":"Szczegóły profilu najemcy", + "no-tenant-profiles-text":"Nie znaleziono profili najemców", + "name-max-length":"Nazwa powinna zawierać mniej niż 256 znaków", + "search":"Szukaj profili najemców", + "selected-tenant-profiles":"{ count, plural, =1 {1 profil najemcy} other {# profile najemców} } wybrano", + "no-tenant-profiles-matching":"Nie znaleziono profili najemców pasujących do '{{entity}}'", + "tenant-profile-required":"Profil najemcy jest wymagany", + "idCopiedMessage":"Id profilu najemcy zostało skopiowane do schowka", + "set-default":"Ustaw profil najemcy jako domyślny", + "delete":"Usuń profil najemcy", + "copyId":"Kopiuj Id profilu najemcy", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "data":"Dane profilu", + "profile-configuration":"Konfiguracja profilu", + "description":"Opis", + "default":"Domyślny", + "delete-tenant-profile-title":"Czy na pewno chcesz usunąć profil najemcy '{{tenantProfileName}}'?", + "delete-tenant-profile-text":"Bądź ostrożny, po potwierdzeniu profil najemcy i wszystkie związane z nim dane zostaną bezpowrotnie usunięte.", + "delete-tenant-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil najemcy} other {# profile najemców} }?", + "delete-tenant-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile najemców zostaną usunięte, a wszystkie związane z nimi dane zostaną bezpowrotnie usunięte.", + "set-default-tenant-profile-title":"Czy na pewno chcesz ustawić profil najemcy '{{tenantProfileName}}' jako domyślny?", + "set-default-tenant-profile-text":"Po potwierdzeniu profil najemcy zostanie oznaczony jako domyślny i będzie używany dla nowych najemców bez określonego profilu.", + "no-tenant-profiles-found":"Nie znaleziono profili najemców.", + "create-new-tenant-profile":"Utwórz nowy!", + "create-tenant-profile":"Utwórz nowy profil najemcy", + "import":"Importuj profil najemcy", + "export":"Eksportuj profil najemcy", + "export-failed-error":"Nie można wyeksportować profilu najemcy: {{error}}", + "tenant-profile-file":"Plik profilu najemcy", + "invalid-tenant-profile-file-error":"Nie można zaimportować profilu najemcy: Nieprawidłowa struktura danych profilu najemcy.", + "advanced-settings":"Ustawienia zaawansowane", + "entities":"Encje", + "rule-engine":"Silnik reguł", + "time-to-live":"Czas życia", + "alarms-and-notifications":"Alarmy i powiadomienia", + "ota-files-in-bytes":"Pliki", + "ws-title":"WS", + "unlimited":"(0 - nielimitowany)", + "maximum-devices":"Maksymalna liczba urządzeń", + "maximum-devices-required":"Maksymalna liczba urządzeń jest wymagana.", + "maximum-devices-range":"Maksymalna liczba urządzeń nie może być ujemna", + "maximum-assets":"Maksymalna liczba zasobów", + "maximum-assets-required":"Maksymalna liczba zasobów jest wymagana.", + "maximum-assets-range":"Maksymalna liczba zasobów nie może być ujemna", + "maximum-customers":"Maksymalna liczba klientów", + "maximum-customers-required":"Maksymalna liczba klientów jest wymagana.", + "maximum-customers-range":"Maksymalna liczba klientów nie może być ujemna", + "maximum-users":"Maksymalna liczba użytkowników", + "maximum-users-required":"Maksymalna liczba użytkowników jest wymagana.", + "maximum-users-range":"Maksymalna liczba użytkowników nie może być ujemna", + "maximum-dashboards":"Maksymalna liczba desek rozdzielczych", + "maximum-dashboards-required":"Maksymalna liczba desek rozdzielczych jest wymagana.", + "maximum-dashboards-range":"Maksymalna liczba desek rozdzielczych nie może być ujemna", + "maximum-rule-chains":"Maksymalna liczba łańcuchów reguł", + "maximum-rule-chains-required":"Maksymalna liczba łańcuchów reguł jest wymagana.", + "maximum-rule-chains-range":"Maksymalna liczba łańcuchów reguł nie może być ujemna", + "maximum-resources-sum-data-size":"Maksymalny łączny rozmiar plików zasobów (bajty)", + "maximum-resources-sum-data-size-required":"Maksymalny łączny rozmiar plików zasobów jest wymagany.", + "maximum-resources-sum-data-size-range":"Maksymalny łączny rozmiar plików zasobów nie może być ujemny", + "maximum-resource-size":"Maksymalny rozmiar pliku zasobu (bajty)", + "maximum-resource-size-required":"Maksymalny rozmiar pliku zasobu jest wymagany", + "maximum-resource-size-range":"Maksymalny rozmiar pliku zasobu nie może być ujemny", + "maximum-ota-packages-sum-data-size":"Maksymalny łączny rozmiar plików pakietów OTA (bajty)", + "maximum-ota-package-sum-data-size-required":"Maksymalny łączny rozmiar plików pakietów OTA jest wymagany.", + "maximum-ota-package-sum-data-size-range":"Maksymalny łączny rozmiar plików pakietów OTA nie może być ujemny", + "rest-requests-for-tenant":"Zapytania REST dla najemcy", + "transport-tenant-telemetry-msg-rate-limit":"Limit wiadomości telemetrii najemcy", + "transport-tenant-telemetry-data-points-rate-limit":"Limit punktów danych telemetrii najemcy", + "transport-device-msg-rate-limit":"Limit wiadomości urządzenia", + "transport-device-telemetry-msg-rate-limit":"Limit wiadomości telemetrii urządzenia", + "transport-device-telemetry-data-points-rate-limit":"Limit punktów danych telemetrii urządzenia", + "tenant-entity-export-rate-limit":"Limit eksportu wersji encji", + "tenant-entity-import-rate-limit":"Limit ładowania wersji encji", + "tenant-notification-request-rate-limit":"Limit żądań powiadomień", + "tenant-notification-requests-per-rule-rate-limit":"Limit żądań powiadomień na regułę powiadomień", + "max-transport-messages":"Maksymalna liczba wiadomości transportowych", + "max-transport-messages-required":"Maksymalna liczba wiadomości transportowych jest wymagana.", + "max-transport-messages-range":"Maksymalna liczba wiadomości transportowych nie może być ujemna", + "max-transport-data-points":"Maksymalna liczba punktów danych transportowych", + "max-transport-data-points-required":"Maksymalna liczba punktów danych transportowych jest wymagana.", + "max-transport-data-points-range":"Maksymalna liczba punktów danych transportowych nie może być ujemna", + "max-r-e-executions":"Maksymalna liczba wykonanych operacji Silnika Reguł", + "max-r-e-executions-required":"Maksymalna liczba wykonanych operacji Silnika Reguł jest wymagana.", + "max-r-e-executions-range":"Maksymalna liczba wykonanych operacji Silnika Reguł nie może być ujemna", + "max-j-s-executions":"Maksymalna liczba wykonanych operacji JavaScript", + "max-j-s-executions-required":"Maksymalna liczba wykonanych operacji JavaScript jest wymagana.", + "max-j-s-executions-range":"Maksymalna liczba wykonanych operacji JavaScript nie może być ujemna", + "max-tbel-executions":"Maksymalna liczba wykonanych operacji TBEL", + "max-tbel-executions-required":"Maksymalna liczba wykonanych operacji TBEL jest wymagana.", + "max-tbel-executions-range":"Maksymalna liczba wykonanych operacji TBEL nie może być ujemna", + "max-d-p-storage-days":"Maksymalna liczba dni przechowywania punktów danych", + "max-d-p-storage-days-required":"Maksymalna liczba dni przechowywania punktów danych jest wymagana.", + "max-d-p-storage-days-range":"Maksymalna liczba dni przechowywania punktów danych nie może być ujemna", + "default-storage-ttl-days":"Domyślny czas życia pamięci masowej w dniach", + "default-storage-ttl-days-required":"Domyślny czas życia pamięci masowej w dniach jest wymagany.", + "default-storage-ttl-days-range":"Domyślny czas życia pamięci masowej w dniach nie może być ujemny", + "alarms-ttl-days":"Czas życia alarmów w dniach", + "alarms-ttl-days-required":"Czas życia alarmów jest wymagany", + "alarms-ttl-days-days-range":"Okres przechowywania alarmów (TTL) nie może być ujemny", + "rpc-ttl-days":"RPC Okres przechowywania (TTL) dni", + "rpc-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla RPC", + "rpc-ttl-days-days-range":"Okres przechowywania (TTL) dni dla RPC nie może być ujemny", + "queue-stats-ttl-days":"Okres przechowywania (TTL) dni statystyk kolejki", + "queue-stats-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla statystyk kolejki", + "queue-stats-ttl-days-range":"Okres przechowywania (TTL) dni dla statystyk kolejki nie może być ujemny", + "rule-engine-exceptions-ttl-days":"Okres przechowywania (TTL) dni wyjątków silnika reguł", + "rule-engine-exceptions-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla wyjątków silnika reguł", + "rule-engine-exceptions-ttl-days-range":"Okres przechowywania (TTL) dni dla wyjątków silnika reguł nie może być ujemny", + "max-rule-node-executions-per-message":"Maksymalna liczba wykonanych węzłów reguł na wiadomość", + "max-rule-node-executions-per-message-required":"Wymagana maksymalna liczba wykonanych węzłów reguł na wiadomość", + "max-rule-node-executions-per-message-range":"Maksymalna liczba wykonanych węzłów reguł na wiadomość nie może być ujemna", + "max-emails":"Maksymalna liczba wysłanych wiadomości e-mail", + "max-emails-required":"Wymagana maksymalna liczba wysłanych wiadomości e-mail", + "max-emails-range":"Maksymalna liczba wysłanych wiadomości e-mail nie może być ujemna", + "sms-enabled":"SMS włączony", + "max-sms":"Maksymalna liczba wysłanych wiadomości SMS", + "max-sms-required":"Wymagana maksymalna liczba wysłanych wiadomości SMS", + "max-sms-range":"Maksymalna liczba wysłanych wiadomości SMS nie może być ujemna", + "max-created-alarms":"Maksymalna liczba utworzonych alarmów", + "max-created-alarms-required":"Wymagana maksymalna liczba utworzonych alarmów", + "max-created-alarms-range":"Maksymalna liczba utworzonych alarmów nie może być ujemna", + "no-queue":"Brak skonfigurowanej kolejki", + "add-queue":"Dodaj kolejkę", + "queues-with-count":"Kolejki ({{count}})", + "tenant-rest-limits":"Żądania REST dla najemcy", + "customer-rest-limits":"Żądania REST dla klienta", + "incorrect-pattern-for-rate-limits":"Nieprawidłowy format dla limitów szybkości. Pary pojemność:okres (w sekundach) oddzielone przecinkiem, np. 100:1,2000:60", + "too-small-value-zero":"Wartość musi być większa niż 0", + "too-small-value-one":"Wartość musi być większa niż 1", + "queue-size-is-limited-by-system-configuration":"Rozmiar kolejki jest również ograniczony konfiguracją systemu.", + "cassandra-tenant-limits-configuration":"Zapytanie do Cassandry dla najemcy", + "ws-limit-max-sessions-per-tenant":"Maksymalna liczba sesji na najemcę", + "ws-limit-max-sessions-per-customer":"Maksymalna liczba sesji na klienta", + "ws-limit-max-sessions-per-regular-user":"Maksymalna liczba sesji na zwykłego użytkownika", + "ws-limit-max-sessions-per-public-user":"Maksymalna liczba sesji na publicznego użytkownika", + "ws-limit-queue-per-session":"Maksymalny rozmiar kolejki wiadomości na sesję", + "ws-limit-max-subscriptions-per-tenant":"Maksymalna liczba subskrypcji na najemcę", + "ws-limit-max-subscriptions-per-customer":"Maksymalna liczba subskrypcji na klienta", + "ws-limit-max-subscriptions-per-regular-user":"Maksymalna liczba subskrypcji na zwykłego użytkownika", + "ws-limit-max-subscriptions-per-public-user":"Maksymalna liczba subskrypcji na publicznego użytkownika", + "ws-limit-updates-per-session":"Aktualizacje WS na sesję", + "rate-limits":{ + "add-limit":"Dodaj limit", + "advanced-settings":"Zaawansowane ustawienia", + "edit-limit":"Edytuj limit", + "but-less-than":"ale mniejsze niż", + "edit-transport-tenant-msg-title":"Edytuj limity szybkości przesyłania komunikatów najemcy", + "edit-transport-tenant-telemetry-msg-title":"Edytuj limity szybkości przesyłania telemetrii komunikatów najemcy", + "edit-transport-tenant-telemetry-data-points-title":"Edytuj limity szybkości przesyłania punktów danych telemetrii najemcy", + "edit-transport-device-msg-title":"Edytuj limity szybkości przesyłania komunikatów urządzenia", + "edit-transport-device-telemetry-msg-title":"Edytuj limity szybkości przesyłania telemetrii komunikatów urządzenia", + "edit-transport-device-telemetry-data-points-title":"Edytuj limity szybkości przesyłania punktów danych telemetrii urządzenia", + "edit-tenant-rest-limits-title":"Edytuj limity szybkości żądań REST dla najemcy", + "edit-customer-rest-limits-title":"Edytuj limity szybkości żądań REST dla klienta", + "edit-ws-limit-updates-per-session-title":"Edytuj limity szybkości aktualizacji WS na sesję", + "edit-cassandra-tenant-limits-configuration-title":"Edytuj limity szybkości zapytań do Cassandry dla najemcy", + "edit-tenant-entity-export-rate-limit-title":"Edytuj limity szybkości tworzenia wersji jednostki dla najemcy", + "edit-tenant-entity-import-rate-limit-title":"Edytuj limity szybkości wczytywania wersji jednostki dla najemcy", + "edit-tenant-notification-request-rate-limit-title":"Edytuj limity szybkości żądań powiadomień dla najemcy", + "edit-tenant-notification-requests-per-rule-rate-limit-title":"Edytuj limity szybkości żądań powiadomień na regułę powiadomień dla najemcy", + "messages-per":"komunikatów na", + "not-set":"Nie ustawiono", + "number-of-messages":"Liczba komunikatów", + "number-of-messages-required":"Wymagana liczba komunikatów.", + "number-of-messages-min":"Minimalna wartość to 1.", + "preview":"Podgląd", + "per-seconds":"Na sekundę", + "per-seconds-required":"Wymagana jest szybkość czasowa.", + "per-seconds-min":"Minimalna wartość to 1.", + "rate-limits":"Limity szybkości", + "remove-limit":"Usuń limit", + "transport-tenant-msg":"Przesyłanie komunikatów najemcy", + "transport-tenant-telemetry-msg":"Przesyłanie telemetrii komunikatów najemcy", + "transport-tenant-telemetry-data-points":"Przesyłanie punktów danych telemetrii najemcy", + "transport-device-msg":"Przesyłanie komunikatów urządzenia", + "transport-device-telemetry-msg":"Przesyłanie telemetrii komunikatów urządzenia", + "transport-device-telemetry-data-points":"Przesyłanie punktów danych telemetrii urządzenia", + "sec":"sek" + } + }, + "timeinterval":{ + "seconds-interval":"{ seconds, plural, =1 {1 sekunda} other {# sekundy} }", + "minutes-interval":"{ minutes, plural, =1 {1 minuta} other {# minuty} }", + "hours-interval":"{ hours, plural, =1 {1 godzina} other {# godziny} }", + "days-interval":"{ days, plural, =1 {1 dzień} other {# dni} }", + "days":"Dni", + "hours":"Godziny", + "minutes":"Minuty", + "seconds":"Sekundy", + "advanced":"Zaawansowane", + "predefined":{ + "yesterday":"Wczoraj", + "day-before-yesterday":"Przedwczoraj", + "this-day-last-week":"Ten dzień w zeszłym tygodniu", + "previous-week":"Poprzedni tydzień (Ndz - Sob)", + "previous-week-iso":"Poprzedni tydzień (Pon - Ndz)", + "previous-month":"Poprzedni miesiąc", + "previous-quarter":"Poprzedni kwartał", + "previous-half-year":"Poprzednie półrocze", + "previous-year":"Poprzedni rok", + "current-hour":"Bieżąca godzina", + "current-day":"Bieżący dzień", + "current-day-so-far":"Bieżący dzień do tej pory", + "current-week":"Bieżący tydzień (Ndz - Sob)", + "current-week-iso":"Bieżący tydzień (Pon - Ndz)", + "current-week-so-far":"Bieżący tydzień do tej pory (Ndz - Sob)", + "current-week-iso-so-far":"Bieżący tydzień do tej pory (Pon - Ndz)", + "current-month":"Bieżący miesiąc", + "current-month-so-far":"Bieżący miesiąc do tej pory", + "current-quarter":"Bieżący kwartał", + "current-quarter-so-far":"Bieżący kwartał do tej pory", + "current-half-year":"Bieżące półrocze", + "current-half-year-so-far":"Bieżące półrocze do tej pory", + "current-year":"Bieżący rok", + "current-year-so-far":"Bieżący rok do tej pory" + }, + "type":{ + "week":"Tydzień (Ndz - Sob)", + "week-iso":"Tydzień (Pon - Ndz)", + "month":"Miesiąc", + "quarter":"Kwartał" + } + }, + "timeunit":{ + "milliseconds":"Milisekundy", + "seconds":"Sekundy", + "minutes":"Minuty", + "hours":"Godziny", + "days":"Dni" + }, + "timewindow":{ + "timewindow":"Okno czasowe", + "years":"{ years, plural, =1 { rok } other {# lata } }", + "years-short":"{{ years }}r", + "months":"{ months, plural, =1 { miesiąc } other {# miesiące } }", + "months-short":"{{ months }}m", + "weeks":"{ weeks, plural, =1 { tydzień } other {# tygodnie } }", + "weeks-short":"{{ weeks }}t", + "days":"{ days, plural, =1 { dzień } other {# dni } }", + "days-short":"{{ days }}d", + "hours":"{ hours, plural, =0 { godzina } =1 {1 godzina } other {# godziny } }", + "hr":"{{ hr }} godz", + "hr-short":"{{ hr }}h", + "minutes":"{ minutes, plural, =0 { minuta } =1 {1 minuta } other {# minuty } }", + "min":"{{ min }} min", + "min-short":"{{ min }}m", + "seconds":"{ seconds, plural, =0 { sekunda } =1 {1 sekunda } other {# sekundy } }", + "sec":"{{ sec }} sek", + "sec-short":"{{ sec }}s", + "short":{ + "days":"{ days, plural, =1 {1 dzień } other {# dni } }", + "hours":"{ hours, plural, =1 {1 godzina } other {# godziny } }", + "minutes":"{{minutes}} min ", + "seconds":"{{seconds}} sek " + }, + "realtime":"Na żywo", + "history":"Historia", + "last-prefix":"ostatni", + "period":"od {{ startTime }} do {{ endTime }}", + "edit":"Edytuj okno czasowe", + "date-range":"Zakres dat", + "for-all-time":"Dla wszystkich dostępnych danych", + "last":"Ostatnie", + "time-period":"Okres czasu", + "hide":"Ukryj", + "interval":"Interwał", + "just-now":"Teraz", + "just-now-lower":"teraz", + "ago":"temu", + "style":"Styl okna czasowego", + "icon":"Ikona", + "icon-position":"Pozycja ikony", + "icon-position-left":"Lewo", + "icon-position-right":"Prawo", + "font":"Czcionka", + "color":"Kolor", + "displayTypePrefix":"Wyświetlaj przedrostek Na żywo/Historia", + "preview":"Podgląd" + }, + "tooltip":{ + "value":"Wartość", + "date":"Data", + "background-color":"Kolor tła", + "background-blur":"Rozmycie tła" + }, + "unit":{ + "millimeter":"Milimetr", + "centimeter":"Centymetr", + "angstrom":"Angstrom", + "nanometer":"Nanometr", + "micrometer":"Mikrometr", + "meter":"Metr", + "kilometer":"Kilometr", + "inch":"Cal", + "foot":"Stopa", + "yard":"Jard", + "mile":"Mila", + "nautical-mile":"Mila morska", + "astronomical-unit":"Jednostka astronomiczna", + "reciprocal-metre":"Odwrócony metr", + "meter-per-meter":"Metr na metr", + "steradian":"Steradian", + "thou":"Thou", + "barleycorn":"Jęczmień", + "hand":"Dłoń", + "chain":"Ciąg", + "furlong":"Furlong", + "league":"Liga", + "fathom":"Fathom", + "cable":"Kabel", + "link":"Link", + "rod":"Ród", + "nanogram":"Nanogram", + "microgram":"Mikrogram", + "milligram":"Miligram", + "gram":"Gram", + "kilogram":"Kilogram", + "tonne":"Tonna", + "ounce":"Uncja", + "pound":"Funt", + "stone":"Stoun", + "hundredweight-count":"Cental", + "short-tons":"Krótka tona", + "dalton":"Dalton", + "grain":"Gran", + "drachm":"Drachma", + "quarter":"Quarter", + "slug":"Slug", + "carat":"Karad", + "cubic-millimeter":"Milimetr sześcienny", + "cubic-centimeter":"Centymetr sześcienny", + "cubic-meter":"Metr sześcienny", + "cubic-kilometer":"Kilometr sześcienny", + "microliter":"Mikrolitr", + "milliliter":"Mililitr", + "liter":"Litr", + "hectoliter":"Hektolitr", + "cubic-inch":"Cal sześcienny", + "cubic-foot":"Stopa sześcienna", + "cubic-yard":"Jard sześcienny", + "fluid-ounce":"Uncja płynu", + "pint":"Pinta", + "quart":"Kwarta", + "gallon":"Galona", + "oil-barrels":"Baryłki naftowe", + "cubic-meter-per-kilogram":"Metr sześcienny na kilogram", + "gill":"Gill", + "hogshead":"Hogshead", + "teaspoon":"Łyżeczka", + "tablespoon":"Łyżka stołowa", + "cup":"Kubek", + "celsius":"Stopnie Celsiusza", + "kelvin":"Kelwin", + "rankine":"Rankine", + "fahrenheit":"Stopnie Fahrenheita", + "percent":"Procent", + "meter-per-second":"Metr na sekundę", + "kilometer-per-hour":"Kilometr na godzinę", + "foot-per-second":"Stopa na sekundę", + "mile-per-hour":"Mila na godzinę", + "knot":"Węzeł", + "millimeters-per-minute":"Milimetrów na minutę", + "kilometer-per-hour-squared":"Kilometr na godzinę do kwadratu", + "foot-per-second-squared":"Stopa na sekundę do kwadratu", + "pascal":"Pascal", + "kilopascal":"Kilopascal", + "megapascal":"Megapascal", + "gigapascal":"Gigapascal", + "millibar":"Milibar", + "bar":"Bar", + "kilobar":"Kilobar", + "newton":"Newton", + "newton-meter":"Newton-metr", + "foot-pounds":"Stopy funtów", + "inch-pounds":"Calowe funty", + "newton-per-meter":"Newton na metr", + "atmospheres":"Atmosfery", + "pounds-per-square-inch":"Funtów na cal kwadratowy", + "torr":"Torr", + "inches-of-mercury":"Cale rtęci", + "pascal-per-square-meter":"Pascal na metr kwadratowy", + "pound-per-square-inch":"Funt na cal kwadratowy", + "newton-per-square-meter":"Newton na metr kwadratowy", + "kilogram-force-per-square-meter":"Kilogram-siła na metr kwadratowy", + "pascal-per-square-centimeter":"Pascal na centymetr kwadratowy", + "ton-force-per-square-inch":"Ton-siła na cal kwadratowy", + "kilonewton-per-square-meter":"Kilonewton na metr kwadratowy", + "newton-per-square-millimeter":"Newton na milimetr kwadratowy", + "microjoule":"Mikrodżul", + "millijoule":"Milidżul", + "joule":"Dżul", + "kilojoule":"Kilodżul", + "megajoule":"Megadżul", + "gigajoule":"Gigadżul", + "watt-hour":"Watogodzina", + "kilowatt-hour":"Kilowatogodzina", + "electron-volts":"Elektronowolt", + "joules-per-coulomb":"Dżule na kulomb", + "british-thermal-unit":"Brytyjska jednostka ciepła", + "foot-pound":"Stopa-funt", + "calorie":"Kaloria", + "small-calorie":"Mała kaloria", + "kilocalorie":"Kilokaloria", + "joule-per-kelvin":"Dżul na kelwin", + "joule-per-kilogram-kelvin":"Dżul na kilogram-kelwin", + "joule-per-kilogram":"Dżul na kilogram", + "watt-per-meter-kelvin":"Wat na metr-kelwin", + "joule-per-cubic-meter":"Dżul na metr sześcienny", + "therm":"Term", + "electric-dipole-moment":"Moment dipolowy elektryczny", + "magnetic-dipole-moment":"Moment dipolowy magnetyczny", + "debye":"Debye", + "coulomb-per-square-meter-per-volt":"Kulomb na metr kwadratowy na wolt", + "milliwatt":"Miliwat", + "microwatt":"Mikrowat", + "watt":"Wat", + "kilowatt":"Kilowat", + "megawatt":"Megawat", + "gigawatt":"Gigawat", + "metric-horsepower":"Koń mechaniczny metryczny", + "milliwatt-per-square-centimeter":"Miliwaty na centymetr kwadratowy", + "watt-per-square-centimeter":"Waty na centymetr kwadratowy", + "kilowatt-per-square-centimeter":"Kilowat na centymetr kwadratowy", + "milliwatt-per-square-meter":"Miliwaty na metr kwadratowy", + "watt-per-square-meter":"Waty na metr kwadratowy", + "kilowatt-per-square-meter":"Kilowaty na metr kwadratowy", + "watt-per-square-inch":"Waty na cal kwadratowy", + "kilowatt-per-square-inch":"Kilowaty na cal kwadratowy", + "horsepower":"Koń mechaniczny", + "btu-per-hour":"Brytyjskie jednostki ciepła na godzinę", + "coulomb":"Kulomb", + "millicoulomb":"Milikulomb", + "microcoulomb":"Mikrokulomb", + "picocoulomb":"Pikokulomb", + "coulomb-per-meter":"Kulomb na metr", + "coulomb-per-cubic-meter":"Kulomb na metr sześcienny", + "coulomb-per-square-meter":"Kulomb na metr kwadratowy", + "square-millimeter":"Metr kwadratowy", + "square-centimeter":"Centymetr kwadratowy", + "square-meter":"Metr kwadratowy", + "hectare":"Hektar", + "square-kilometer":"Kilometr kwadratowy", + "square-inch":"Cal kwadratowy", + "square-foot":"Stopa kwadratowa", + "square-yard":"Jard kwadratowy", + "acre":"Akr", + "square-mile":"Mila kwadratowa", + "are":"Ar", + "barn":"Barn", + "circular-inch":"Cal okrągły", + "milliampere-hour":"Miliamperogodzina", + "milliampere-hour-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, godzina miliamperowa, miliamperogodziny, mAh", + "ampere-hours":"Amperogodziny", + "ampere-hours-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, amper, amperogodziny, Ah", + "kiloampere-hours":"Kiloamperogodziny", + "kiloampere-hours-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, kiloamperogodziny, kiloamperogodzina, kAh", + "nanoampere":"Nanoamper", + "nanoampere-tags":"prąd, ampery, nanoamper, nA", + "picoampere":"Pikoamper", + "picoampere-tags":"prąd, ampery, pikoamper, pA", + "microampere":"Mikroamper", + "microampere-tags":"prąd elektryczny, mikroamper, mikroampery, μA", + "milliampere":"Miliamper", + "milliampere-tags":"prąd elektryczny, miliamper, miliampery, mA", + "ampere":"Amper", + "ampere-tags":"prąd elektryczny, przepływ prądu, przepływ elektryczności, przepływ elektryczny, amper, ampery, amperaż, A", + "kiloamperes":"Kiloampery", + "kiloamperes-tags":"prąd elektryczny, przepływ prądu, kiloampery, kA", + "microampere-per-square-centimeter":"Mikroamper na centymetr kwadratowy", + "microampere-per-square-centimeter-tags":"Gęstość prądu, mikroamper na centymetr kwadratowy, µA/cm²", + "ampere-per-square-meter":"Amper na metr kwadratowy", + "ampere-per-square-meter-tags":"gęstość prądu, prąd na jednostkę powierzchni, amper na metr kwadratowy, A/m²", + "ampere-per-meter":"Amper na metr", + "ampere-per-meter-tags":"siła pola magnetycznego, intensywność pola magnetycznego, amper na metr, A/m", + "oersted":"Oersted", + "oersted-tags":"pole magnetyczne, oersted, Oe", + "bohr-magneton":"Magnetony Bohra", + "bohr-magneton-tags":"fizyka atomowa, moment magnetyczny, magneton Bohra, μB", + "ampere-meter-squared":"Amper-Metr Kwadratowy", + "ampere-meter-squared-tags":"moment magnetyczny, moment dipolowy, amper-metr kwadratowy, A·m²", + "ampere-meter":"Amper-Metr", + "ampere-meter-tags":"pole magnetyczne, pętla prądowa, amper-metr, A·m", + "nanovolt":"Nanowolt", + "picovolt":"Pikowolt", + "millivolts":"Milivolty", + "microvolts":"Mikrowolty", + "volt":"Wolt", + "kilovolts":"Kilowolty", + "dbmV":"dBmV", + "dbm":"dBm", + "volt-meter":"Woltomierz", + "kilovolt-meter":"Kilowoltomierz", + "megavolt-meter":"Megawoltomierz", + "microvolt-meter":"Mikrowoltomierz", + "millivolt-meter":"Milivoltomierz", + "nanovolt-meter":"Nanowoltomierz", + "ohm":"Om", + "microohm":"Mikroom", + "milliohm":"Miloom", + "kilohm":"Kiloom", + "megohm":"Megaom", + "gigohm":"Gigaom", + "hertz":"Herz", + "kilohertz":"Kilohertz", + "megahertz":"Megahertz", + "gigahertz":"Gigahertz", + "rpm":"Obroty na minutę", + "candela-per-square-meter":"Kandela na metr kwadratowy", + "candela":"Kandela", + "lumen":"Lumen", + "lux":"Luks", + "foot-candle":"Stopa-świeca", + "lumen-per-square-meter":"Lumen na metr kwadratowy", + "lux-second":"Luks-sekunda", + "lumen-second":"Lumen-sekunda", + "lumens-per-watt":"Lumeny na wat", + "absorbance":"Absorpcja", + "mole":"Mol", + "nanomole":"Nanomol", + "micromole":"Mikromol", + "millimole":"Milimol", + "kilomole":"Kilomol", + "mole-per-cubic-meter":"Mol na metr sześcienny", + "rssi":"RSSI", + "ppm":"Części na milion", + "ppb":"Części na miliard", + "micrograms-per-cubic-meter":"Mikrogramy na metr sześcienny", + "aqi":"AQI", + "gram-per-cubic-meter":"Gram na metr sześcienny", + "gram-per-kilogram":"Wilgotność właściwa", + "millimeters-per-second":"Milimetry na sekundę", + "neper":"Neper", + "bel":"Bel", + "decibel":"Decybel", + "meters-per-second-squared":"Metry na sekundę kwadratową", + "becquerel":"Bekerel", + "curie":"Kiri", + "gray":"Graj", + "sievert":"Sivert", + "roentgen":"Rentgen", + "cps":"Zliczenia na sekundę", + "rad":"Rad", + "rem":"Rem", + "dps":"Rozpady na sekundę", + "rutherford":"Rutherford", + "coulombs-per-kilogram":"Kulomby na kilogram", + "becquerels-per-cubic-meter":"Bekerel na metr sześcienny", + "curies-per-liter":"Kiri na litr", + "becquerels-per-second":"Bekerel na sekundę", + "curies-per-second":"Kiri na sekundę", + "gy-per-second":"Graj na sekundę", + "watt-per-steradian":"Wat na steradian", + "watt-per-square-metre-steradian":"Wat na metr kwadratowy-steradian", + "ph-level":"Poziom pH", + "turbidity":"Mętność", + "mg-per-liter":"Miligramy na litr", + "microsiemens-per-centimeter":"Mikrosiemeny na centymetr", + "millisiemens-per-meter":"Milisiemeny na metr", + "siemens-per-meter":"Siemeny na metr", + "kilogram-per-cubic-meter":"Kilogram na metr sześcienny", + "gram-per-cubic-centimeter":"Gram na centymetr sześcienny", + "kilogram-per-square-meter":"Kilogram na metr kwadratowy", + "milligram-per-milliliter":"Miligram na mililitr", + "milligram-per-cubic-meter":"Miligram na metr sześcienny", + "pound-per-cubic-foot":"Funt na stopę sześcienną", + "ounces-per-cubic-inch":"Uncje na cal sześcienny", + "tons-per-cubic-yard":"Tony na jard sześcienny", + "particle-density":"Gęstość cząstek", + "kilometers-per-liter":"Kilometry na litr", + "miles-per-gallon":"Mile na galon", + "liters-per-100-km":"Litrów na 100 kilometrów", + "gallons-per-mile":"Galony na milę", + "liters-per-hour":"Litrów na godzinę", + "gallons-per-hour":"Galony na godzinę", + "beats-per-minute":"Uderzenia na minutę", + "millimeters-of-mercury":"Milimetry słupa rtęci", + "milligrams-per-deciliter":"Miligramy na decylitr", + "g-force":"Siła G", + "kilonewton":"Kiloniuton", + "kilogram-force":"Kilogram-siła", + "pound-force":"Funt-siła", + "kilopound-force":"Kilofunt-siła", + "dyne":"Dyn", + "poundal":"Poundal", + "kip":"Kip", + "gal":"Gal", + "gravity":"Przyspieszenie grawitacyjne", + "hectopascal":"Hektopaskal", + "atmosphere":"Atmosfera", + "millibars":"Milibary", + "inch-of-mercury":"Cal słupa rtęci", + "richter-scale":"Skala Richtera", + "second":"Sekunda", + "minute":"Minuta", + "hour":"Godzina", + "day":"Dzień", + "week":"Tydzień", + "month":"Miesiąc", + "year":"Rok", + "cubic-foot-per-minute":"Stopy sześcienne na minutę", + "cubic-meters-per-hour":"Metry sześcienne na godzinę", + "cubic-meters-per-second":"Metry sześcienne na sekundę", + "liter-per-second":"Litr na sekundę", + "liter-per-minute":"Litr na minutę", + "gallons-per-minute":"Galony na minutę", + "cubic-foot-per-second":"Stopa sześcienna na sekundę", + "milliliters-per-minute":"Mililitrów na minutę", + "bit":"Bit", + "byte":"Bajt", + "kilobyte":"Kilobajt", + "megabyte":"Megabajt", + "gigabyte":"Gigabajt", + "terabyte":"Terabajt", + "petabyte":"Petabajt", + "exabyte":"Eksabajt", + "zettabyte":"Zettabajt", + "yottabyte":"Jottabajt", + "bit-per-second":"Bit na sekundę", + "kilobit-per-second":"Kilobit na sekundę", + "megabit-per-second":"Megabit na sekundę", + "gigabit-per-second":"Gigabit na sekundę", + "terabit-per-second":"Terabit na sekundę", + "byte-per-second":"Bajt na sekundę", + "kilobyte-per-second":"Kilobajt na sekundę", + "megabyte-per-second":"Megabajt na sekundę", + "gigabyte-per-second":"Gigabajt na sekundę", + "degree":"Stopień", + "radian":"Radian", + "gradian":"Grad", + "mil":"Mil", + "revolution":"Obrotów", + "siemens":"Siemens", + "millisiemens":"Milisiemens", + "microsiemens":"Mikrosiemens", + "kilosiemens":"Kilosiemens", + "megasiemens":"Megasiemens", + "gigasiemens":"Gigasiemens", + "farad":"Farad", + "millifarad":"Milifarad", + "microfarad":"Mikrofarad", + "nanofarad":"Nanofarad", + "picofarad":"Pikofarad", + "kilofarad":"Kilofarad", + "megafarad":"Megafarad", + "gigafarad":"Gigafarad", + "terfarad":"Terfarad", + "farad-per-meter":"Farad na metr", + "tesla":"Tesla", + "gauss":"Gaus", + "kilogauss":"Kilogaus", + "millitesla":"Millitesla", + "microtesla":"Mikrotesla", + "nanotesla":"Nanotesla", + "kilotesla":"Kilotesla", + "megatesla":"Megatesla", + "millitesla-square-meters":"Millitesla na metry kwadratowe", + "gamma":"Gamma", + "lambda":"Lambda", + "square-meter-per-second":"Metr kwadratowy na sekundę", + "square-centimeter-per-second":"Centymetr kwadratowy na sekundę", + "stoke":"Stoke", + "centistokes":"Centistoke", + "square-foot-per-second":"Stopa kwadratowa na sekundę", + "square-inch-per-second":"Cal kwadratowy na sekundę", + "pascal-second":"Paskal-sekunda", + "centipoise":"Centipoise", + "poise":"Poise", + "reynolds":"Reynolds", + "pound-per-foot-hour":"Funt na stopę kwadratową na godzinę", + "newton-second-per-square-meter":"Newton-sekunda na metr kwadratowy", + "dyne-second-per-square-centimeter":"Dyn-sekunda na centymetr kwadratowy", + "kilogram-per-meter-second":"Kilogram na metr-sekundę", + "tesla-square-meters":"Tesla na metry kwadratowe", + "maxwell":"Maxwell", + "tesla-per-meter":"Tesla na metr", + "gauss-per-centimeter":"Gaus na centymetr", + "weber":"Weber", + "microweber":"Mikroweber", + "milliweber":"Milliweber", + "gauss-square-centimeter":"Gaus na centymetr kwadratowy", + "kilogauss-square-centimeter":"Kilogaus na centymetr kwadratowy", + "henry":"Henry", + "millihenry":"Millihenry", + "microhenry":"Mikrohenry", + "nanohenry":"Nanohenry", + "henry-per-meter":"Henry na metr", + "tesla-meter-per-ampere":"Tesla na metr na amper", + "gauss-per-oersted":"Gaus na Oersted", + "kilogram-per-mole":"Kilogram na mol", + "gram-per-mole":"Gram na mol", + "milligram-per-mole":"Miligram na mol", + "joule-per-mole":"Dżul na mol", + "joule-per-mole-kelvin":"Dżul na mol-kelwin", + "millivolts-per-meter":"Milivolty na metr", + "volts-per-meter":"Volty na metr", + "kilovolts-per-meter":"Kilovolty na metr", + "radian-per-second":"Radian na sekundę", + "radian-per-second-squared":"Radian na sekundę kwadratową", + "revolutions-per-minute-per-second":"Przyspieszenie kątowe", + "revolutions-per-minute-per-second-squared":"Przyspieszenie kątowe", + "deg-per-second":"Stopnie na sekundę", + "degrees-brix":"Stopnie Brix", + "katal":"Katal", + "katal-per-cubic-metre":"Katal na metr sześcienny" + }, + "user":{ + "user":"Użytkownik", + "users":"Użytkownicy", + "customer-users":"Użytkownicy klienta", + "tenant-admins":"Administratorzy najemcy", + "sys-admin":"Administrator systemu", + "tenant-admin":"Administrator najemcy", + "customer":"Klient", + "anonymous":"Anonimowy", + "add":"Dodaj użytkownika", + "delete":"Usuń użytkownika", + "add-user-text":"Dodaj nowego użytkownika", + "no-users-text":"Nie znaleziono użytkowników", + "user-details":"Szczegóły użytkownika", + "delete-user-title":"Czy na pewno chcesz usunąć użytkownika '{{userEmail}}'?", + "delete-user-text":"Uważaj, po potwierdzeniu użytkownik i wszystkie powiązane dane staną się nieodwracalne.", + "delete-users-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 użytkownika} other {# użytkowników} }?", + "delete-users-action-title":"Usuń { count, plural, =1 {1 użytkownika} other {# użytkowników} }", + "delete-users-text":"Uważaj, po potwierdzeniu wszyscy wybrani użytkownicy zostaną usunięci, a wszystkie powiązane dane staną się nieodwracalne.", + "activation-email-sent-message":"E-mail aktywacyjny został wysłany pomyślnie!", + "resend-activation":"Wyślij ponownie aktywację", + "email":"Email", + "email-required":"Email jest wymagany.", + "invalid-email-format":"Nieprawidłowy format email.", + "first-name":"Imię", + "last-name":"Nazwisko", + "description":"Opis", + "default-dashboard":"Domyślny pulpit", + "always-fullscreen":"Zawsze na pełnym ekranie", + "select-user":"Wybierz użytkownika", + "no-users-matching":"Nie znaleziono użytkowników pasujących do '{{entity}}'.", + "user-required":"Użytkownik jest wymagany", + "activation-method":"Metoda aktywacji", + "display-activation-link":"Wyświetl link aktywacyjny", + "send-activation-mail":"Wyślij mail aktywacyjny", + "activation-link":"Link aktywacyjny użytkownika", + "activation-link-text":"Aby aktywować użytkownika, użyj następującego linku aktywacyjnego:", + "copy-activation-link":"Kopiuj link aktywacyjny", + "activation-link-copied-message":"Link aktywacyjny użytkownika został skopiowany do schowka", + "details":"Szczegóły", + "login-as-tenant-admin":"Zaloguj jako Administrator Najemcy", + "login-as-customer-user":"Zaloguj jako Użytkownik Klienta", + "search":"Szukaj użytkowników", + "selected-users":"{ count, plural, =1 {1 użytkownik} other {# użytkowników} } wybrano", + "disable-account":"Wyłącz konto użytkownika", + "enable-account":"Włącz konto użytkownika", + "enable-account-message":"Konto użytkownika zostało pomyślnie włączone!", + "disable-account-message":"Konto użytkownika zostało pomyślnie wyłączone!", + "copyId":"Kopiuj Id użytkownika", + "idCopiedMessage":"Id użytkownika zostało skopiowane do schowka", + "user-list":"Lista użytkowników", + "user-list-required":"Lista użytkowników jest wymagana" + }, + "value":{ + "type":"Typ wartości", + "string":"Ciąg znaków", + "string-value":"Wartość ciągu znaków", + "string-value-required":"Wartość ciągu znaków jest wymagana", + "integer":"Liczba całkowita", + "integer-value":"Wartość liczby całkowitej", + "integer-value-required":"Wartość liczby całkowitej jest wymagana", + "invalid-integer-value":"Nieprawidłowa wartość liczby całkowitej", + "double":"Podwójna", + "double-value":"Wartość podwójna", + "double-value-required":"Wartość podwójna jest wymagana", + "boolean":"Boolean", + "boolean-value":"Wartość Boolean", + "false":"Fałsz", + "true":"Prawda", + "long":"Długa", + "json":"JSON", + "json-value":"Wartość JSON", + "json-value-invalid":"Wartość JSON ma nieprawidłowy format", + "json-value-required":"Wartość JSON jest wymagana." + }, + "version-control":{ + "version-control":"Kontrola wersji", + "management":"Zarządzanie kontrolą wersji", + "search":"Szukaj wersji", + "branch":"Gałąź", + "default":"Domyślna", + "select-branch":"Wybierz gałąź", + "branch-required":"Gałąź jest wymagana", + "create-entity-version":"Utwórz wersję encji", + "version-name":"Nazwa wersji", + "version-name-required":"Nazwa wersji jest wymagana", + "author":"Autor", + "export-relations":"Eksportuj relacje", + "export-attributes":"Eksportuj atrybuty", + "export-credentials":"Eksportuj poświadczenia", + "entity-versions":"Wersje encji", + "versions":"Wersje", + "created-time":"Czas utworzenia", + "version-id":"ID wersji", + "no-entity-versions-text":"Nie znaleziono wersji encji", + "no-versions-text":"Nie znaleziono wersji", + "copy-full-version-id":"Kopiuj pełne ID wersji", + "create-version":"Utwórz wersję", + "creating-version":"Tworzenie wersji... Proszę czekać", + "nothing-to-commit":"Brak zmian do zatwierdzenia", + "restore-version":"Przywróć wersję", + "restore-entity-from-version":"Przywróć encję z wersji '{{versionName}}'", + "restoring-entity-version":"Przywracanie wersji encji... Proszę czekać", + "load-relations":"Załaduj relacje", + "load-attributes":"Załaduj atrybuty", + "load-credentials":"Załaduj poświadczenia", + "compare-with-current":"Porównaj z obecną", + "diff-entity-with-version":"Różnice z wersją encji '{{versionName}}'", + "previous-difference":"Poprzednia różnica", + "next-difference":"Następna różnica", + "current":"Obecna", + "differences":"{ count, plural, =1 {1 różnica} other {# różnice} }", + "create-entities-version":"Utwórz wersję encji", + "default-sync-strategy":"Domyślna strategia synchronizacji", + "sync-strategy-merge":"Scal", + "sync-strategy-overwrite":"Nadpisz", + "entities-to-export":"Encje do eksportu", + "entities-to-restore":"Encje do przywrócenia", + "sync-strategy":"Strategia synchronizacji", + "all-entities":"Wszystkie encje", + "no-entities-to-export-prompt":"Proszę określić encje do eksportu", + "no-entities-to-restore-prompt":"Proszę określić encje do przywrócenia", + "add-entity-type":"Dodaj typ encji", + "remove-all":"Usuń wszystko", + "version-create-result":"{ added, plural, =0 {Nie dodano encji} =1 {Dodano 1 encję} other {Dodano # encji} }.
{ modified, plural, =0 {Nie zmodyfikowano encji} =1 {Zmodyfikowano 1 encję} other {Zmodyfikowano # encji} }.
{ removed, plural, =0 {Nie usunięto encji} =1 {Usunięto 1 encję} other {Usunięto # encji} }.", + "remove-other-entities":"Usuń inne encje", + "find-existing-entity-by-name":"Znajdź istniejącą encję po nazwie", + "restore-entities-from-version":"Przywróć encje z wersji '{{versionName}}'", + "restoring-entities-from-version":"Przywracanie encji... Proszę czekać", + "no-entities-restored":"Nie przywrócono żadnych encji", + "created":"{{created}} utworzonych", + "updated":"{{updated}} zaktualizowanych", + "deleted":"{{deleted}} usuniętych", + "remove-other-entities-confirm-text":"Uważaj! To trwale usunie wszystkie bieżące encje
nieobecne w wersji, którą chcesz przywrócić.

Proszę wpisać \"usuń inne encje\", aby potwierdzić.", + "auto-commit-to-branch":"automatyczne zatwierdzenie do gałęzi {{ branch }}", + "default-create-entity-version-name":"Aktualizacja {{entityName}}", + "sync-strategy-merge-hint":"Tworzy lub aktualizuje wybrane encje w repozytorium. Wszystkie inne encje w repozytorium są niezmodyfikowane.", + "sync-strategy-overwrite-hint":"Tworzy lub aktualizuje wybrane encje w repozytorium. Wszystkie inne encje w repozytorium są usunięte.", + "device-credentials-conflict":"Nie udało się załadować urządzenia z zewnętrznym id {{entityId}}
ponieważ te same poświadczenia są już obecne w bazie danych dla innego urządzenia.
Rozważ wyłączenie opcji załaduj poświadczenia w formularzu przywracania.", + "missing-referenced-entity":"Nie udało się załadować {{sourceEntityTypeName}} z zewnętrznym id {{sourceEntityId}}
ponieważ odnosi się do brakującej {{targetEntityTypeName}} z id {{targetEntityId}}.", + "runtime-failed":"Failed: {{message}}", + "auto-commit-settings-read-only-hint":"Funkcja automatycznego zatwierdzania nie działa z włączoną opcją tylko do odczytu w ustawieniach repozytorium." + }, + "widget":{ + "widget-library":"Biblioteka widgetów", + "widget-bundle":"Pakiet widgetów", + "all-bundles":"Wszystkie pakiety", + "select-widgets-bundle":"Wybierz pakiet widgetów", + "widgets":"Widgety", + "all-widgets":"Wszystkie widgety", + "widget":"Widget", + "select-widget":"Wybierz widget", + "no-widgets-matching":"Nie znaleziono widgetów pasujących do '{{entity}}'.", + "no-widgets":"Brak widgetów", + "no-widgets-text":"Nie znaleziono widgetów", + "management":"Zarządzanie widgetami", + "editor":"Edytor widgetów", + "confirm-to-exit-editor-html":"Masz niezapisane ustawienia widgetu.
Czy na pewno chcesz opuścić tę stronę?", + "widget-type-not-found":"Problem z ładowaniem konfiguracji widgetu.
Prawdopodobnie powiązany typ widgetu został usunięty.", + "widget-type-load-error":"Widget nie został załadowany z powodu następujących błędów:", + "remove":"Usuń widget", + "delete":"Usuń widget", + "edit":"Edytuj widget", + "remove-widget-title":"Czy na pewno chcesz usunąć widget '{{widgetTitle}}'?", + "remove-widget-text":"Po potwierdzeniu widget i wszystkie powiązane dane staną się nieodwracalne.", + "timeseries":"Serie czasowe", + "search-data":"Szukaj danych", + "no-data-found":"Nie znaleziono danych", + "latest":"Najnowsze wartości", + "rpc":"Widget sterowania", + "alarm":"Widget alarmów", + "static":"Widget statyczny", + "timeseries-short":"serie", + "latest-short":"najnowsze", + "rpc-short":"sterowanie", + "alarm-short":"alarm", + "static-short":"statyczny", + "select-widget-type":"Wybierz typ widgetu", + "missing-widget-title-error":"Tytuł widgetu musi być określony!", + "widget-saved":"Widget zapisany", + "unable-to-save-widget-error":"Nie można zapisać widgetu! Widget zawiera błędy!", + "save":"Zapisz widget", + "saveAs":"Zapisz widget jako", + "move":"Przenieś widget", + "save-widget-as":"Zapisz widget jako", + "save-widget-as-text":"Proszę wprowadzić nowy tytuł widgetu", + "toggle-fullscreen":"Przełącz pełny ekran", + "run":"Uruchom widget", + "widget-title":"Tytuł widgetu", + "title":"Tytuł", + "title-required":"Tytuł widgetu jest wymagany.", + "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", + "system":"System", + "type":"Typ widgetu", + "resources":"Zasoby", + "resource-url":"URL JavaScript/CSS", + "resource-is-module":"Jest modułem", + "remove-resource":"Usuń zasób", + "add-resource":"Dodaj zasób", + "html":"HTML", + "tidy":"Uporządkuj", + "css":"CSS", + "settings-schema":"Schemat ustawień", + "datakey-settings-schema":"Schemat ustawień klucza danych", + "latest-datakey-settings-schema":"Schemat ustawień najnowszego klucza danych", + "widget-settings":"Ustawienia widgetu", + "description":"Opis", + "tags":"Tagi", + "image-preview":"Podgląd obrazu", + "settings-form-selector":"Selektor formularza ustawień", + "data-key-settings-form-selector":"Selektor formularza ustawień klucza danych", + "latest-data-key-settings-form-selector":"Selektor formularza ustawień najnowszego klucza danych", + "all":"Wszystkie", + "actual":"Aktualne", + "deprecated":"Przestarzałe", + "has-basic-mode":"Ma tryb podstawowy", + "basic-mode-form-selector":"Selektor formularza trybu podstawowego", + "basic-mode":"Podstawowy", + "advanced-mode":"Zaawansowany", + "javascript":"Javascript", + "js":"JS", + "delete-widget-title":"Czy na pewno chcesz usunąć widget '{{widgetName}}'?", + "delete-widget-text":"Po potwierdzeniu widget i wszystkie powiązane dane staną się nieodwracalne.", + "delete-widgets-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 widget} other {# widgetów} }?", + "delete-widgets-text":"Uważaj, po potwierdzeniu wszystkie wybrane widgety zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", + "delete-widget":"Usuń widget", + "widget-template-load-failed-error":"Nie udało się załadować szablonu widgetu!", + "details":"Szczegóły", + "widget-details":"Szczegóły widgetu", + "add":"Dodaj widget", + "add-existing-widget":"Dodaj istniejący widget", + "add-new-widget":"Dodaj nowy widget", + "search-widgets":"Szukaj widgetów", + "selected-widgets":"{ count, plural, =1 {1 widget} other {# widgetów} } wybrano", + "undo":"Cofnij zmiany widgetu", + "export":"Eksportuj widget", + "export-widgets":"Eksportuj widgety", + "import":"Importuj widget", + "no-data":"Brak danych do wyświetlenia na widżecie", + "data-overflow":"Widget wyświetla {{count}} z {{total}} encji", + "alarm-data-overflow":"Widget wyświetla alarmy dla {{allowedEntities}} (maksymalna dozwolona) encji z {{totalEntities}} encji", + "search":"Szukaj widgetu", + "filter":"Typ filtru widgetu", + "loading-widgets":"Ładowanie widgetów...", + "widget-template-error":"Nieprawidłowy szablon HTML widgetu." + }, + "widget-action":{ + "header-button":"Przycisk nagłówka widgetu", + "open-dashboard-state":"Przejdź do nowego stanu pulpitów", + "update-dashboard-state":"Zaktualizuj bieżący stan pulpitów", + "open-dashboard":"Przejdź do innego pulpitu", + "custom":"Akcja niestandardowa", + "custom-pretty":"Akcja niestandardowa (z szablonem HTML)", + "custom-pretty-error-title":"Błąd niestandardowego okna dialogowego", + "custom-pretty-template-error":"Nieprawidłowy szablon niestandardowego okna dialogowego.", + "custom-pretty-controller-error":"Wystąpił błąd podczas oceny funkcji niestandardowego okna dialogowego.", + "mobile-action":"Akcja mobilna", + "target-dashboard-state":"Docelowy stan pulpitu", + "target-dashboard-state-required":"Docelowy stan pulpitu jest wymagany", + "set-entity-from-widget":"Ustaw encję z widgetu", + "target-dashboard":"Docelowy pulpit", + "open-right-layout":"Otwórz prawy układ pulpitu (widok mobilny)", + "state-display-type":"Opcja wyświetlania stanu pulpitu", + "open-normal":"Normalnie", + "open-in-separate-dialog":"Otwórz w osobnym oknie dialogowym", + "open-in-popover":"Otwórz w dymku", + "dialog-title":"Tytuł okna dialogowego", + "dialog-hide-dashboard-toolbar":"Ukryj pasek narzędzi pulpitu w oknie dialogowym", + "dialog-width":"Szerokość okna dialogowego w procentach względem szerokości viewportu", + "dialog-height":"Wysokość okna dialogowego w procentach względem wysokości viewportu", + "dialog-size-range-error":"Wartość procentowa rozmiaru okna dialogowego powinna być w zakresie od 1 do 100.", + "popover-preferred-placement":"Preferowane umiejscowienie dymka", + "popover-placement-top":"Góra", + "popover-placement-topLeft":"Góra lewo", + "popover-placement-topRight":"Góra prawo", + "popover-placement-right":"Prawo", + "popover-placement-rightTop":"Prawo góra", + "popover-placement-rightBottom":"Prawo dół", + "popover-placement-bottom":"Dół", + "popover-placement-bottomLeft":"Dół lewo", + "popover-placement-bottomRight":"Dół prawo", + "popover-placement-left":"Lewo", + "popover-placement-leftTop":"Lewo góra", + "popover-placement-leftBottom":"Lewo dół", + "popover-hide-on-click-outside":"Ukryj dymek po kliknięciu na zewnątrz", + "popover-hide-dashboard-toolbar":"Ukryj pasek narzędzi pulpitu w dymku", + "popover-width":"Szerokość dymka w jednostkach przeglądarki (np. 100px, 25vw)", + "popover-height":"Wysokość dymka w jednostkach przeglądarki (np. 100px, 25vh)", + "popover-style":"Styl dymka", + "open-new-browser-tab":"Otwórz w nowej karcie przeglądarki", + "mobile":{ + "action-type":"Typ akcji mobilnej", + "action-type-required":"Typ akcji mobilnej jest wymagany", + "take-picture-from-gallery":"Wybierz zdjęcie z galerii", + "take-photo":"Zrób zdjęcie", + "map-direction":"Otwórz wskazówki dojazdu na mapie", + "map-location":"Otwórz lokalizację na mapie", + "scan-qr-code":"Skanuj kod QR", + "make-phone-call":"Wykonaj połączenie telefoniczne", + "get-location":"Pobierz lokalizację telefonu", + "take-screenshot":"Zrób zrzut ekranu" + } + }, + "widgets-bundle":{ + "current":"Aktualny zestaw", + "widgets-bundles":"Zestawy widżetów", + "widgets-bundle-widgets":"Widżety zestawu widżetów", + "add":"Dodaj zestaw widżetów", + "delete":"Usuń zestaw widżetów", + "title":"Tytuł", + "title-required":"Tytuł jest wymagany.", + "title-max-length":"Tytuł powinien być krótszy niż 256", + "description":"Opis", + "image-preview":"Podgląd obrazu", + "order":"Kolejność", + "add-widgets-bundle-text":"Dodaj nowy zestaw widżetów", + "no-widgets-bundles-text":"Nie znaleziono zestawów widżetów", + "empty":"Zestaw widżetów jest pusty", + "details":"Szczegóły", + "widgets-bundle-details":"Szczegóły zestawu widżetów", + "delete-widgets-bundle-title":"Czy na pewno chcesz usunąć zestaw widżetów '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text":"Uważaj, po potwierdzeniu zestaw widżetów oraz wszystkie powiązane dane staną się nieodwracalne.", + "delete-widgets-bundles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 zestaw widżetów} other {# zestawy widżetów} }?", + "delete-widgets-bundles-action-title":"Usuń { count, plural, =1 {1 zestaw widżetów} other {# zestawy widżetów} }", + "delete-widgets-bundles-text":"Uważaj, po potwierdzeniu wszystkie wybrane zestawy widżetów zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", + "no-widgets-bundles-matching":"Nie znaleziono zestawów widżetów pasujących do '{{widgetsBundle}}'.", + "widgets-bundle-required":"Zestaw widżetów jest wymagany.", + "system":"System", + "import":"Importuj zestaw widżetów", + "export":"Eksportuj zestaw widżetów", + "export-widgets-bundle-widgets-prompt":"Dołącz widżety zestawu w eksportowanych danych (w przeciwnym razie zostaną wyeksportowane tylko odniesienia do pełnych kwalifikacji nazw widżetów)", + "export-failed-error":"Nie można wyeksportować zestawu widżetów: {{error}}", + "create-new-widgets-bundle":"Utwórz nowy zestaw widżetów", + "widgets-bundle-file":"Plik zestawu widżetów", + "invalid-widgets-bundle-file-error":"Nie można zaimportować zestawu widżetów: Nieprawidłowa struktura danych zestawu widżetów.", + "search":"Szukaj zestawów widżetów", + "selected-widgets-bundles":"{ count, plural, =1 {1 wybrany zestaw widżetów} other {# wybrane zestawy widżetów} }", + "open-widgets-bundle":"Otwórz zestaw widżetów", + "loading-widgets-bundles":"Ładowanie zestawów widżetów..." + }, + "widget-config":{ + "data":"Dane", + "settings":"Ustawienia", + "advanced":"Zaawansowane", + "appearance":"Wygląd", + "widget-card":"Karta widżetu", + "mobile":"Mobilny", + "title":"Tytuł", + "title-tooltip":"Podpowiedź tytułu", + "general-settings":"Ustawienia ogólne", + "display-title":"Wyświetl tytuł widżetu", + "card-title":"Tytuł karty", + "drop-shadow":"Cień", + "enable-fullscreen":"Włącz pełny ekran", + "background-color":"Kolor tła", + "text-color":"Kolor tekstu", + "border-radius":"Promień obramowania", + "padding":"Odstęp", + "margin":"Margines", + "widget-style":"Styl widżetu", + "widget-css":"CSS widżetu", + "title-style":"Styl tytułu", + "mobile-mode-settings":"Ustawienia trybu mobilnego", + "order":"Kolejność", + "height":"Wysokość", + "mobile-hide":"Ukryj widżet w trybie mobilnym", + "desktop-hide":"Ukryj widżet w trybie desktopowym", + "units":"Specjalny symbol obok wartości", + "units-by-default":"Domyślne jednostki", + "decimals":"Liczba miejsc po przecinku", + "decimals-by-default":"Domyślna liczba miejsc po przecinku", + "default-data-key-parameter-hint":"Ten parametr dotyczy wszystkich wartości widżetu, chyba że zostanie nadpisany przez konfigurację klucza danych", + "units-short":"Jednostki", + "decimals-short":"Miejsca po przecinku", + "decimals-suffix":"miejsca po przecinku", + "timewindow":"Okno czasowe", + "use-dashboard-timewindow":"Użyj okna czasowego dashboardu", + "use-widget-timewindow":"Użyj okna czasowego widżetu", + "display-timewindow":"Wyświetl okno czasowe", + "legend":"Legenda", + "display-legend":"Wyświetl legendę", + "datasources":"Źródła danych", + "datasource":"Źródło danych", + "maximum-datasources":"Maksymalnie { count, plural, =1 {1 źródło danych jest dozwolone.} other {# źródeł danych jest dozwolonych} }", + "timeseries-key-error":"Należy określić co najmniej jeden klucz danych szeregów czasowych", + "datasource-type":"Typ", + "datasource-parameters":"Parametry", + "remove-datasource":"Usuń źródło danych", + "add-datasource":"Dodaj źródło danych", + "target-device":"Docelowe urządzenie", + "alarm-source":"Źródło alarmu", + "actions":"Akcje", + "action":"Akcja", + "add-action":"Dodaj akcję", + "search-actions":"Szukaj akcji", + "no-actions-text":"Nie znaleziono akcji", + "action-source":"Źródło akcji", + "action-source-required":"Źródło akcji jest wymagane.", + "action-name":"Nazwa", + "action-name-required":"Nazwa akcji jest wymagana.", + "action-name-not-unique":"Inna akcja o tej samej nazwie już istnieje.
Nazwa akcji powin.", + "action-icon":"Ikona", + "show-hide-action-using-function":"Pokaż/ukryj akcję za pomocą funkcji", + "action-type":"Typ", + "action-type-required":"Typ akcji jest wymagany.", + "edit-action":"Edytuj akcję", + "delete-action":"Usuń akcję", + "delete-action-title":"Usuń akcję widżetu", + "delete-action-text":"Czy na pewno chcesz usunąć akcję widżetu o nazwie '{{actionName}}'?", + "title-icon":"Ikona tytułu", + "display-icon":"Wyświetl ikonę tytułu", + "card-icon":"Ikona karty", + "icon":"Ikona", + "icon-color":"Kolor ikony", + "icon-size":"Rozmiar ikony", + "advanced-settings":"Zaawansowane ustawienia", + "data-settings":"Ustawienia danych", + "limits":"Limity", + "no-data-display-message":"Alternatywny komunikat \"Brak danych do wyświetlenia\"", + "data-page-size":"Maksymalna liczba encji na źródło danych", + "settings-component-not-found":"Nie znaleziono komponentu formularza ustawień dla selektora '{{selector}}'", + "preview":"Podgląd", + "set":"Ustaw", + "set-message":"Ustaw komunikat", + "advanced-title-style":"Zaawansowany styl tytułu", + "card-style":"Styl karty", + "text":"Tekst", + "background":"Tło", + "advanced-widget-style":"Zaawansowany styl widżetu", + "card-buttons":"Przyciski karty", + "show-card-buttons":"Pokaż przyciski karty", + "card-border-radius":"Promień obramowania karty", + "card-appearance":"Wygląd karty", + "color":"Kolor", + "tooltip":"Podpowiedź", + "units-required":"Jednostka jest wymagana." + }, + "widget-type":{ + "import":"Importuj typ widżetu", + "export":"Eksportuj typ widżetu", + "export-failed-error":"Nie można wyeksportować widżetu: {{error}}", + "widget-file":"Plik widżetu", + "invalid-widget-file-error":"Nie można zaimportować widżetu: Nieprawidłowa struktura danych widżetu." + }, + "widgets":{ + "background":{ + "background":"Tło", + "background-settings":"Ustawienia tła", + "background-type-image":"Obraz", + "background-type-color":"Kolor", + "image-url":"URL obrazu", + "overlay":"Nakładka", + "enable-overlay":"Włącz nakładkę", + "blur":"Rozmycie", + "preview":"Podgląd" + }, + "bar-chart":{ + "bar-appearance":"Wygląd słupka", + "label-on-bar":"Etykieta na słupku", + "value-on-bar":"Wartość na słupku", + "bar-chart-card-style":"Styl karty wykresu słupkowego" + }, + "battery-level":{ + "layout":"Układ", + "layout-vertical-solid":"Pionowy. Stały", + "layout-horizontal-solid":"Poziomy. Stały", + "layout-vertical-divided":"Pionowy. Podzielony", + "layout-horizontal-divided":"Poziomy. Podzielony", + "icon":"Ikona", + "value":"Wartość", + "auto-scale":"Automatyczne skalowanie", + "battery-level-color":"Kolor poziomu baterii", + "battery-shape-color":"Kolor kształtu baterii", + "battery-level-card-style":"Styl karty poziomu baterii", + "sections-count":"Liczba sekcji" + }, + "signal-strength":{ + "value":"Wartość", + "last-update":"Ostatnia aktualizacja", + "no-signal":"Brak sygnału", + "layout":"Układ", + "layout-wifi":"Wi-Fi", + "layout-cellular-bar":"Słupkowy komórkowy", + "icon":"Ikona", + "date":"Data", + "active-bars-color":"Kolor aktywnych pasków sygnału", + "inactive-bars-color":"Kolor nieaktywnych pasków sygnału", + "signal-strength-card-style":"Styl karty siły sygnału" + }, + "chart":{ + "common-settings":"Wspólne ustawienia", + "enable-stacking-mode":"Włącz tryb układania", + "selection":"Wybór zakresu czasowego", + "enable-selection-mode":"Włącz tryb wyboru", + "line-shadow-size":"Rozmiar cienia linii", + "display-smooth-lines":"Wyświetlaj gładkie (krzywe) linie", + "default-bar-width":"Domyślna szerokość słupka dla danych niezagregowanych (milisekundy)", + "bar-alignment":"Wyrównanie słupka", + "bar-alignment-left":"Lewo", + "bar-alignment-right":"Prawo", + "bar-alignment-center":"Środek", + "default-font":"Domyślna czcionka", + "default-font-size":"Domyślny rozmiar czcionki", + "default-font-color":"Domyślny kolor czcionki", + "thresholds-line-width":"Domyślna szerokość linii dla wszystkich progów", + "tooltip-settings":"Ustawienia podpowiedzi", + "tooltip":"Podpowiedź", + "show-tooltip":"Pokaż podpowiedź", + "hover-individual-points":"Najedź na pojedyncze punkty", + "show-cumulative-values":"Pokaż wartości kumulatywne w trybie układania", + "hide-zero-false-values":"Ukryj zera/fałszywe wartości z podpowiedzi", + "tooltip-value-format-function":"Funkcja formatująca wartość podpowiedzi", + "grid-settings":"Ustawienia siatki", + "show-vertical-lines":"Pokaż linie pionowe", + "show-horizontal-lines":"Pokaż linie poziome", + "grid-outline-border-width":"Szerokość krawędzi/ramki siatki (px)", + "primary-color":"Kolor podstawowy", + "background-color":"Kolor tła", + "ticks-color":"Kolor znaczników", + "xaxis-settings":"Ustawienia osi X", + "axis-title":"Tytuł osi", + "xaxis-tick-labels-settings":"Ustawienia etykiet osi X", + "show-tick-labels":"Pokaż etykiety osi", + "yaxis-settings":"Ustawienia osi Y", + "min-scale-value":"Minimalna wartość na skali", + "max-scale-value":"Maksymalna wartość na skali", + "yaxis-tick-labels-settings":"Ustawienia etykiet osi Y", + "tick-step-size":"Rozmiar kroku między znacznikami", + "number-of-decimals":"Liczba miejsc po przecinku do wyświetlenia", + "ticks-formatter-function":"Funkcja formatująca znaczniki", + "comparison-settings":"Ustawienia porównywania", + "enable-comparison":"Włącz porównywanie", + "time-for-comparison":"Okres porównywania", + "time-for-comparison-previous-interval":"Poprzedni interwał (domyślnie)", + "time-for-comparison-days":"Dzień temu", + "time-for-comparison-weeks":"Tydzień temu", + "time-for-comparison-months":"Miesiąc temu", + "time-for-comparison-years":"Rok temu", + "time-for-comparison-custom-interval":"Niestandardowy interwał", + "custom-interval-value":"Wartość niestandardowego interwału (ms)", + "comparison-x-axis-settings":"Ustawienia osi X porównywania", + "axis-position":"Pozycja osi", + "axis-position-top":"Góra (domyślnie)", + "axis-position-bottom":"Dół", + "custom-legend-settings":"Niestandardowe ustawienia legendy", + "enable-custom-legend":"Włącz niestandardową legendę (pozwoli to na korzystanie z atrybutów/czasu rzeczywistego w etykietach kluczy)", + "key-name":"Nazwa klucza", + "key-name-required":"Nazwa klucza jest wymagana", + "key-type":"Typ klucza", + "key-type-attribute":"Atrybut", + "key-type-timeseries":"Czas rzeczywisty", + "label-keys-list":"Lista kluczy do użycia w etykietach", + "no-label-keys":"Brak skonfigurowanych kluczy", + "add-label-key":"Dodaj nowy klucz", + "line-width":"Szerokość linii", + "color":"Kolor", + "data-is-hidden-by-default":"Dane są domyślnie ukryte", + "disable-data-hiding":"Wyłącz ukrywanie danych", + "remove-from-legend":"Usuń klucz danych z legendy", + "exclude-from-stacking":"Wyłącz z układania (dostępne w trybie „Układanie”)", + "line-settings":"Ustawienia linii", + "show-line":"Pokaż linię", + "fill-line":"Wypełnij linię", + "fill-line-opacity":"Przezroczystość wypełnienia linii", + "points-settings":"Ustawienia punktów", + "show-points":"Pokaż punkty", + "points-line-width":"Szerokość linii punktów", + "points-radius":"Promień punktów", + "point-shape":"Kształt punktu", + "point-shape-circle":"Okrąg", + "point-shape-cross":"Krzyż", + "point-shape-diamond":"Diament", + "point-shape-square":"Kwadrat", + "point-shape-triangle":"Trójkąt", + "point-shape-custom":"Niestandardowa funkcja", + "point-shape-draw-function":"Funkcja rysowania kształtu punktu", + "show-separate-axis":"Pokaż oddzielną oś", + "axis-position-left":"Lewo", + "axis-position-right":"Prawo", + "thresholds":"Progi", + "no-thresholds":"Brak skonfigurowanych progów", + "add-threshold":"Dodaj próg", + "show-values-for-comparison":"Pokaż historyczne wartości do porównania", + "comparison-values-label":"Etykieta historycznych wartości", + "comparison-line-color":"Kolor linii porównawczej", + "threshold-settings":"Ustawienia progu", + "use-as-threshold":"Użyj wartości klucza jako progu", + "threshold-line-width":"Szerokość linii progu", + "threshold-color":"Kolor progu", + "common-pie-settings":"Wspólne ustawienia wykresu kołowego", + "radius":"Promień", + "inner-radius":"Wewnętrzny promień", + "tilt":"Nachylenie", + "common-pie-settings-range-error":"Wartość powinna być w zakresie od 0 do 1", + "stroke-settings":"Ustawienia obrysu", + "width-pixels":"Szerokość (piksele)", + "show-labels":"Pokaż etykiety", + "animation-settings":"Ustawienia animacji", + "animated-pie":"Włącz animację kołową (eksperymentalne)", + "border-settings":"Ustawienia obramowania", + "border-width":"Szerokość obramowania", + "border-color":"Kolor obramowania", + "legend-settings":"Ustawienia legendy", + "display-legend":"Wyświetl legendę", + "labels-font-color":"Kolor czcionki etykiet", + "series":"Seria", + "add-series":"Dodaj serię", + "series-settings":"Ustawienia serii", + "remove-series":"Usuń serię", + "no-series":"Brak skonfigurowanych serii", + "no-series-error":"Należy określić co najmniej jedną serię", + "chart-appearance":"Wygląd wykresu", + "vertical-grid-lines":"Linie siatki pionowej", + "horizontal-grid-lines":"Linie siatki poziomej", + "chart-background":"Tło wykresu", + "grid-lines-color":"Kolor linii siatki", + "border":"Obramowanie", + "axis":"Oś", + "vertical-axis":"Oś pionowa", + "ticks":"Znaczniki", + "horizontal-axis":"Oś pozioma" + }, + "color":{ + "color-settings":"Ustawienia koloru", + "color-type-constant":"Stały", + "color-type-range":"Zakres", + "color-type-function":"Funkcja", + "color":"Kolor", + "value-range":"Zakres wartości", + "from":"Od", + "to":"Do", + "color-function":"Funkcja koloru", + "copy-color-settings-from":"Skopiuj ustawienia koloru z" + }, + "dashboard-state":{ + "dashboard-state-settings":"Ustawienia stanu pulpitu", + "dashboard-state":"Identyfikator stanu pulpitu", + "autofill-state-layout":"Domyślne wypełnianie wysokości układu stanu automatycznie", + "default-margin":"Domyślny margines widżetów", + "default-background-color":"Domyślny kolor tła", + "sync-parent-state-params":"Synchronizuj parametry stanu z nadrzędnym pulpitem" + }, + "date-range-picker-settings":{ + "date-range-picker-settings":"Ustawienia wybieraka zakresu dat", + "hide-date-range-picker":"Ukryj wybierak zakresu dat", + "picker-one-panel":"Wybierak zakresu dat jednopanelowy", + "picker-auto-confirm":"Automatyczne potwierdzanie wybieraka zakresu dat", + "picker-show-template":"Pokazuj szablon wybieraka zakresu dat", + "first-day-of-week":"Pierwszy dzień tygodnia", + "interval-settings":"Ustawienia interwału", + "hide-interval":"Ukryj interwał", + "initial-interval":"Początkowy interwał", + "interval-hour":"Godzina", + "interval-day":"Dzień", + "interval-week":"Tydzień", + "interval-two-weeks":"2 tygodnie", + "interval-month":"Miesiąc", + "interval-three-months":"3 miesiące", + "interval-six-months":"6 miesięcy", + "step-settings":"Ustawienia kroku", + "hide-step-size":"Ukryj rozmiar kroku", + "initial-step-size":"Początkowy rozmiar kroku", + "hide-labels":"Ukryj etykiety", + "use-session-storage":"Użyj przechowywania sesji", + "localizationMap":{ + "Sun":"Niedz.", + "Mon":"Pon.", + "Tue":"Wt.", + "Wed":"Śr.", + "Thu":"Czw.", + "Fri":"Piąt.", + "Sat":"Sob.", + "Jan":"Sty", + "Feb":"Lut", + "Mar":"Mar", + "Apr":"Kwi", + "May":"Maj", + "Jun":"Cze", + "Jul":"Lip", + "Aug":"Sie", + "Sep":"Wrz", + "Oct":"Paź", + "Nov":"Lis", + "Dec":"Gru", + "January":"Styczeń", + "February":"Luty", + "March":"Marzec", + "April":"Kwiecień", + "June":"Czerwiec", + "July":"Lipiec", + "August":"Sierpień", + "September":"Wrzesień", + "October":"Październik", + "November":"Listopad", + "December":"Grudzień", + "Custom Date Range":"Niestandardowy zakres dat", + "Date Range Template":"Szablon zakresu dat", + "Today":"Dziś", + "Yesterday":"Wczoraj", + "This Week":"Bieżący tydzień", + "Last Week":"Ostatni tydzień", + "This Month":"Bieżący miesiąc", + "Last Month":"Ostatni miesiąc", + "Year":"Rok", + "This Year":"Bieżący rok", + "Last Year":"Ostatni rok", + "Date picker":"Wybierak dat", + "Hour":"Godzina", + "Day":"Dzień", + "Week":"Tydzień", + "2 weeks":"2 Tygodnie", + "Month":"Miesiąc", + "3 months":"3 Miesiące", + "6 months":"6 Miesięcy", + "Custom interval":"Niestandardowy interwał", + "Interval":"Interwał", + "Step size":"Rozmiar kroku", + "Ok":"Ok" + } + }, + "doughnut":{ + "total":"Suma", + "layout":"Układ", + "layout-default":"Domyślny", + "layout-with-total":"Z sumą", + "auto-scale":"Automatyczne skalowanie", + "clockwise-layout":"Układ zgodny z ruchem wskazówek zegara", + "sort-series":"Sortuj serie według etykiety", + "central-total-value":"Centralna wartość sumy", + "tooltip-value-type-absolute":"Bezwzględna", + "tooltip-value-type-percentage":"Procentowa", + "doughnut-card-style":"Styl karty pączka" + }, + "entities-hierarchy":{ + "hierarchy-data-settings":"Ustawienia danych hierarchii", + "relations-query-function":"Funkcja zapytania o relacje węzła", + "has-children-function":"Funkcja sprawdzająca, czy węzeł ma dzieci", + "node-state-settings":"Ustawienia stanu węzła", + "node-opened-function":"Domyślna funkcja otwierania węzła", + "node-disabled-function":"Funkcja dezaktywacji węzła", + "display-settings":"Ustawienia wyświetlania", + "node-icon-function":"Funkcja ikony węzła", + "node-text-function":"Funkcja tekstu węzła", + "sort-settings":"Ustawienia sortowania", + "nodes-sort-function":"Funkcja sortowania węzłów" + }, + "edge":{ + "display-default-title":"Wyświetl domyślny tytuł" + }, + "gateway":{ + "general-settings":"Ustawienia ogólne", + "widget-title":"Tytuł widżetu", + "default-archive-file-name":"Domyślna nazwa pliku archiwum", + "device-type-for-new-gateway":"Typ urządzenia dla nowej bramy", + "messages-settings":"Ustawienia komunikatów", + "save-config-success-message":"Komunikat tekstowy o pomyślnie zapisanej konfiguracji bramy", + "device-name-exists-message":"Komunikat tekstowy, gdy urządzenie o podanej nazwie już istnieje", + "gateway-title":"Formularz bramy", + "read-only":"Tylko do odczytu", + "events-title":"Tytuł formularza zdarzeń bramy", + "events-filter":"Filtr zdarzeń", + "event-key-contains":"Klucz zdarzenia zawiera...", + "show-connector":"Pokaż dla konektora", + "connector-state-param-key":"Klucz parametru stanu konektora", + "status":"Status", + "message":"Wiadomość", + "created-time":"Czas utworzenia" + }, + "gauge":{ + "default-color":"Domyślny kolor", + "radial-gauge-settings":"Ustawienia miernika radialnego", + "ticks-settings":"Ustawienia podziałek", + "min-value":"Wartość minimalna", + "max-value":"Wartość maksymalna", + "min-value-short":"min", + "max-value-short":"max", + "start-ticks-angle":"Kąt początkowy podziałek", + "ticks-angle":"Kąt podziałek", + "major-ticks":"Podziałki główne", + "major-ticks-count":"Liczba podziałek głównych", + "major-ticks-color":"Kolor podziałek głównych", + "minor-ticks":"Podziałki dodatkowe", + "minor-ticks-count":"Liczba podziałek dodatkowych", + "minor-ticks-color":"Kolor podziałek dodatkowych", + "tick-numbers-font":"Czcionka numerów podziałek", + "unit-title-settings":"Ustawienia jednostki", + "show-unit-title":"Pokaż nazwę jednostki", + "unit-title":"Nazwa jednostki", + "title-font":"Czcionka tytułu", + "units-settings":"Ustawienia jednostek", + "units-font":"Czcionka jednostek", + "value-box-settings":"Ustawienia pola wartości", + "show-value-box":"Pokaż pole wartości", + "value-box":"Pole wartości", + "value-int":"Liczba cyfr dla części całkowitej wartości", + "value-text":"Tekst wartości", + "value-text-shadow":"Cień tekstu wartości", + "value-font":"Czcionka tekstu wartości", + "rect-stroke-color-start":"Kolor obwódki prostokąta - początek gradientu", + "rect-stroke-color-end":"Kolor obwódki prostokąta - koniec gradientu", + "background-color":"Kolor tła", + "shadow-color":"Kolor cienia", + "value-box-rect-stroke-color":"Kolor obwódki prostokąta pola wartości", + "value-box-rect-stroke-color-end":"Kolor obwódki prostokąta pola wartości - koniec gradientu", + "value-box-background-color":"Kolor tła pola wartości", + "value-box-shadow-color":"Kolor cienia pola wartości", + "plate-settings":"Ustawienia tarczy", + "show-plate-border":"Pokaż krawędź tarczy", + "plate-color":"Kolor tarczy", + "needle-settings":"Ustawienia wskazówki", + "needle-circle-size":"Rozmiar koła wskazówki", + "needle-color":"Kolor wskazówki", + "needle-color-start":"Kolor wskazówki - początek gradientu", + "needle-color-end":"Kolor wskazówki - koniec gradientu", + "needle-color-shadow-up":"Kolor górnej części cienia wskazówki", + "needle-color-shadow-down":"Cień", + "highlights-settings":"Ustawienia punktów kulminacyjnych", + "highlights-width":"Szerokość punktów kulminacyjnych", + "highlights":"Punkty kulminacyjne", + "highlight-from":"Od", + "highlight-to":"Do", + "highlight-color":"Kolor", + "no-highlights":"Brak skonfigurowanych punktów kulminacyjnych", + "add-highlight":"Dodaj punkt kulminacyjny", + "animation-settings":"Ustawienia animacji", + "enable-animation":"Animacja", + "animation-duration-rule":"Czas trwania i reguła animacji", + "animation-duration":"Czas trwania animacji", + "animation-rule":"Reguła animacji", + "animation-linear":"Liniowa", + "animation-quad":"Kwadratowa", + "animation-quint":"Kwintowa", + "animation-cycle":"Cykliczna", + "animation-bounce":"Odbijająca się", + "animation-elastic":"Elastyczna", + "animation-dequad":"Odwrócona kwadratowa", + "animation-dequint":"Odwrócona kwintowa", + "animation-decycle":"Odwrócona cykliczna", + "animation-debounce":"Odwrócona odbijająca się", + "animation-delastic":"Odwrócona elastyczna", + "linear-gauge-settings":"Ustawienia miernika liniowego", + "bar-stroke":"Obwódka słupka", + "bar-stroke-width":"Szerokość obwódki słupka", + "bar-stroke-color":"Kolor obwódki słupka", + "bar-background-color":"Kolor tła słupka - początek gradientu", + "bar-background-color-end":"Kolor tła słupka - koniec gradientu", + "progress-bar-color":"Kolor paska postępu", + "progress-bar":"Pasek postępu", + "progress-bar-color-start":"Kolor paska postępu - początek gradientu", + "progress-bar-color-end":"Kolor paska postępu - koniec gradientu", + "major-ticks-names":"Nazwy podziałek głównych", + "show-stroke-ticks":"Pokaż obwódki podziałek", + "major-ticks-font":"Czcionka podziałek głównych", + "border-color":"Kolor obramowania", + "border-width":"Szerokość obramowania", + "needle-circle":"Koło wskazówki", + "needle-circle-color":"Kolor koła wskazówki", + "animation-target":"Cel animacji", + "animation-target-needle":"Wskazówka", + "animation-target-plate":"Tarcza", + "common-settings":"Wspólne ustawienia miernika", + "gauge-type":"Typ miernika", + "gauge-type-arc":"Łuk", + "gauge-type-donut":"Pączek", + "gauge-type-horizontal-bar":"Słupkowy poziomy", + "gauge-type-vertical-bar":"Słupkowy pionowy", + "donut-start-angle":"Kąt początkowy pączka", + "bar-settings":"Ustawienia słupka", + "relative-bar-width":"Względna szerokość słupka", + "neon-glow-brightness":"Jasność efektu świetlnego, (0-100), 0 - wyłączony efekt", + "stripes-thickness":"Grubość pasków, 0 - brak pasków", + "rounded-line-cap":"Użyj zaokrąglonego zakończenia linii", + "bar-color-settings":"Ustawienia koloru słupka", + "use-precise-level-color-values":"Użyj precyzyjnych poziomów kolorów", + "bar-colors":"Kolory słupków, od dolnego do górnego", + "color":"Kolor", + "no-bar-colors":"Brak skonfigurowanych kolorów słupków", + "add-bar-color":"Dodaj kolor słupka", + "from":"Od", + "to":"Do", + "fixed-level-colors":"Kolory słupków z użyciem wartości granicznych", + "gauge-title-settings":"Ustawienia tytułu miernika", + "show-gauge-title":"Pokaż tytuł miernika", + "gauge-title":"Tytuł miernika", + "gauge-title-font":"Czcionka tytułu miernika", + "unit-title-and-timestamp-settings":"Ustawienia jednostki i znacznika czasowego", + "show-timestamp":"Pokaż znacznik czasu wartości", + "timestamp-format":"Format znacznika czasu", + "label-font":"Czcionka etykiety pod wartością", + "value-settings":"Ustawienia wartości", + "show-value":"Pokaż tekst wartości", + "min-max-settings":"Ustawienia etykiet minimalnej/maksymalnej", + "show-min-max":"Pokaż wartości minimalną i maksymalną", + "min-max-font":"Czcionka etykiet minimalnej i maksymalnej", + "show-ticks":"Pokaż podziały", + "tick-width":"Szerokość podziałek", + "tick-color":"Kolor podziałek", + "tick-values":"Wartości podziałek", + "no-tick-values":"Brak skonfigurowanych wartości podziałek", + "add-tick-value":"Dodaj wartość podziałki", + "gauge-appearance":"Wygląd miernika", + "units-title":"Tytuł jednostek", + "value":"Wartość", + "ticks":"Podziały", + "arrow-and-scale-color":"Kolor strzałki i skali domyślnej", + "scale-settings":"Ustawienia skali", + "scale":"Skala", + "scale-color":"Kolory skali", + "compass-appearance":"Wygląd kompasu", + "labels":"Etykiety", + "label-style":"Styl etykiety" + }, + "gpio":{ + "pin":"Pin", + "label":"Label", + "row":"Row", + "column":"Column", + "color":"Color", + "panel-settings":"Panel settings", + "background-color":"Background color", + "gpio-switches":"GPIO switches", + "no-gpio-switches":"No GPIO switches configured", + "add-gpio-switch":"Add GPIO switch", + "gpio-status-request":"GPIO status request", + "method-name":"Method name", + "method-body":"Method body", + "gpio-status-change-request":"GPIO status change request", + "parse-gpio-status-function":"Parse gpio status function", + "gpio-leds":"GPIO leds", + "no-gpio-leds":"No GPIO leds configured", + "add-gpio-led":"Add GPIO led" + }, + "html-card":{ + "html":"HTML", + "css":"CSS" + }, + "input-widgets":{ + "attribute-not-allowed":"Parametr atrybutu nie może być używany w tym widżecie", + "blocked-location":"Geolokalizacja jest zablokowana w twojej przeglądarce", + "claim-device":"Zgłoś urządzenie", + "claim-failed":"Nie udało się zgłosić urządzenia!", + "claim-not-found":"Nie znaleziono urządzenia!", + "claim-successful":"Urządzenie zostało pomyślnie zgłoszone!", + "date":"Data", + "device-name":"Nazwa urządzenia", + "device-name-required":"Wymagana jest nazwa urządzenia", + "discard-changes":"Odrzuć zmiany", + "entity-attribute-required":"Wymagany jest atrybut encji", + "entity-coordinate-required":"Wymagane są obie pola, szerokość geograficzna i długość geograficzna", + "entity-timeseries-required":"Wymagany jest szereg czasowy encji", + "get-location":"Pobierz bieżącą lokalizację", + "invalid-date":"Nieprawidłowa data", + "latitude":"Szerokość geograficzna", + "longitude":"Długość geograficzna", + "min-value-error":"Wartość minimalna to {{value}}", + "max-value-error":"Wartość maksymalna to {{value}}", + "not-allowed-entity":"Wybrana encja nie może mieć współdzielonych atrybutów", + "no-attribute-selected":"Nie wybrano atrybutu", + "no-datakey-selected":"Nie wybrano klucza danych", + "no-coordinate-specified":"Nie określono klucza danych dla szerokości/długości geograficznej", + "no-entity-selected":"Nie wybrano encji", + "no-image":"Brak obrazu", + "no-support-geolocation":"Twoja przeglądarka nie obsługuje geolokalizacji", + "no-support-web-camera":"Twoja przeglądarka nie obsługuje kamer", + "enable-https-use-widget":"Proszę włączyć protokół HTTPS, aby korzystać z tego widżetu", + "no-found-your-camera":"Nie można znaleźć twojej kamery", + "no-permission-camera":"Użytkownik odmówił zgody / Ta witryna nie ma uprawnień do korzystania z kamery", + "no-timeseries-selected":"Nie wybrano szeregu czasowego", + "secret-key":"Tajny klucz", + "secret-key-required":"Wymagany jest tajny klucz", + "switch-attribute-value":"Przełącz wartość atrybutu encji", + "switch-camera":"Przełącz kamerę", + "switch-timeseries-value":"Przełącz wartość szeregu czasowego encji", + "take-photo":"Zrób zdjęcie", + "time":"Czas", + "timeseries-not-allowed":"Parametr szeregu czasowego nie może być używany w tym widżecie", + "update-failed":"Aktualizacja nieudana", + "update-successful":"Aktualizacja udana", + "update-attribute":"Zaktualizuj atrybut", + "update-timeseries":"Zaktualizuj szereg czasowy", + "value":"Wartość", + "general-settings":"Ustawienia ogólne", + "widget-title":"Tytuł widżetu", + "claim-button-label":"Etykieta przycisku zgłaszania", + "show-secret-key-field":"Pokaż pole wprowadzania „Tajny klucz”", + "labels-settings":"Ustawienia etykiet", + "show-labels":"Pokaż etykiety", + "device-name-label":"Etykieta pola wprowadzania nazwy urządzenia", + "secret-key-label":"Etykieta pola wprowadzania tajnego klucza", + "messages-settings":"Ustawienia wiadomości", + "claim-device-success-message":"Komunikat o udanym zgłoszeniu urządzenia", + "claim-device-not-found-message":"Komunikat, gdy urządzenie nie zostało znalezione", + "claim-device-failed-message":"Komunikat o nieudanym zgłoszeniu urządzenia", + "claim-device-name-required-message":"Komunikat błędu „Wymagana nazwa urządzenia”", + "claim-device-secret-key-required-message":"Komunikat błędu „Wymagany tajny klucz”", + "show-label":"Pokaż etykietę", + "label":"Etykieta", + "required":"Wymagane", + "required-error-message":"Komunikat błędu „Wymagane”", + "show-result-message":"Pokaż komunikat z wynikiem", + "integer-field-settings":"Ustawienia pola całkowitego", + "min-value":"Wartość minimalna", + "max-value":"Wartość maksymalna", + "double-field-settings":"Ustawienia pola liczbowego", + "text-field-settings":"Ustawienia pola tekstowego", + "min-length":"Minimalna długość", + "max-length":"Maksymalna długość", + "checkbox-settings":"Ustawienia pola wyboru", + "true-label":"Etykieta zaznaczenia", + "false-label":"Etykieta odznaczenia", + "image-input-settings":"Ustawienia wejścia obrazu", + "display-preview":"Wyświetl podgląd", + "display-clear-button":"Wyświetl przycisk Wyczyść", + "display-apply-button":"Wyświetl przycisk Zastosuj", + "display-discard-button":"Wyświetl przycisk Odrzuć", + "datetime-field-settings":"Ustawienia pola daty/czasu", + "display-time-input":"Wyświetl pole czasowe", + "latitude-key-name":"Nazwa klucza szerokości geograficznej", + "longitude-key-name":"Nazwa klucza długości geograficznej", + "show-get-location-button":"Pokaż przycisk „Pobierz bieżącą lokalizację”", + "use-high-accuracy":"Użyj wysokiej dokładności", + "location-fields-settings":"Ustawienia pól lokalizacji", + "latitude-label":"Etykieta dla szerokości geograficznej", + "longitude-label":"Etykieta dla długości geograficznej", + "input-fields-alignment":"Wyrównanie pól wprowadzania", + "input-fields-alignment-column":"Kolumna (domyślnie)", + "input-fields-alignment-row":"Rząd", + "layout":"Układ", + "row-gap":"Odstęp między rzędami w pikselach", + "column-gap":"Odstęp między kolumnami w pikselach", + "latitude-field-required":"Pole szerokości geograficznej jest wymagane", + "longitude-field-required":"Pole długości geograficznej jest wymagane", + "attribute-settings":"Ustawienia atrybutu", + "widget-mode":"Tryb widżetu", + "widget-mode-update-attribute":"Zaktualizuj atrybut", + "widget-mode-update-timeseries":"Zaktualizuj szereg czasowy", + "attribute-scope":"Zakres atrybutu", + "attribute-scope-server":"Atrybut serwera (domyślnie)", + "attribute-scope-shared":"Współdzielony atrybut", + "value-required":"Wartość wymagana", + "image-settings":"Ustawienia obrazu", + "image-format":"Format obrazu", + "image-format-jpeg":"JPEG", + "image-format-png":"PNG", + "image-format-webp":"WEBP", + "image-quality":"Jakość obrazu stosująca kompresję stratną, np. jpeg i webp", + "max-image-width":"Maksymalna szerokość obrazu", + "max-image-height":"Maksymalna wysokość obrazu", + "action-buttons":"Przyciski akcji", + "show-action-buttons":"Pokaż przyciski akcji", + "update-all-values":"Zaktualizuj wszystkie wartości, nie tylko zmodyfikowane", + "save-button-label":"Etykieta przycisku „ZAPISZ”", + "reset-button-label":"Etykieta przycisku „COFNIJ”", + "group-settings":"Ustawienia grupy", + "show-group-title":"Pokaż tytuł grupy pól związanych z różnymi encjami", + "group-title":"Tytuł grupy", + "fields-alignment":"Wyrównanie pól", + "fields-alignment-row":"Rząd (domyślnie)", + "fields-alignment-column":"Kolumna", + "fields-in-row":"Liczba pól w rzędzie", + "option-value":"Wartość (wpisz „null”, aby utworzyć pustą opcję)", + "option-label":"Etykieta", + "hide-input-field":"Ukryj pole wprowadzania", + "datakey-type":"Typ klucza danych", + "datakey-type-server":"Atrybut serwera (domyślnie)", + "datakey-type-shared":"Współdzielony atrybut", + "datakey-type-timeseries":"Szereg czasowy", + "datakey-value-type":"Typ wartości klucza danych", + "datakey-value-type-string":"Ciąg znaków", + "datakey-value-type-double":"Liczba zmiennoprzecinkowa", + "datakey-value-type-integer":"Liczba całkowita", + "datakey-value-type-json":"JSON", + "datakey-value-type-boolean-checkbox":"Boolean (Pole wyboru)", + "datakey-value-type-boolean-switch":"Boolean (Przełącznik)", + "datakey-value-type-date-time":"Data i godzina", + "datakey-value-type-date":"Data", + "datakey-value-type-time":"Czas", + "datakey-value-type-select":"Wybierz", + "datakey-value-type-color":"Kolor", + "value-is-required":"Wartość jest wymagana", + "ability-to-edit-attribute":"Zdolność do edycji atrybutu", + "ability-to-edit-attribute-editable":"Edytowalny (domyślnie)", + "ability-to-edit-attribute-disabled":"Wyłączony", + "ability-to-edit-attribute-readonly":"Tylko do odczytu", + "disable-on-datakey-name":"Wyłącz na fałszywą wartość innego klucza danych (podaj nazwę klucza danych)", + "field-appearance":"Wygląd pola", + "appearance-fill":"Wypełnienie", + "appearance-outline":"Obrys", + "subscript-sizing":"Rozmiar indeksu dolnego", + "subscript-sizing-fixed":"Stały", + "subscript-sizing-dynamic":"Dynamiczny", + "slide-toggle-settings":"Ustawienia przełącznika", + "slide-toggle-label-position":"Pozycja etykiety przełącznika", + "slide-toggle-label-position-after":"Po", + "slide-toggle-label-position-before":"Przed", + "select-options":"Opcje wyboru", + "no-select-options":"Brak skonfigurowanych opcji wyboru", + "add-select-option":"Dodaj opcję wyboru", + "numeric-field-settings":"Ustawienia pola numerycznego", + "step-interval":"Interwał kroku pomiędzy wartościami", + "error-messages":"Komunikaty o błędach", + "min-value-error-message":"Komunikat o błędzie 'Wartość minimalna'", + "max-value-error-message":"Komunikat o błędzie 'Wartość maksymalna'", + "invalid-date-error-message":"Komunikat o błędzie 'Nieprawidłowa data'", + "invalid-JSON-error-message":"Komunikat o błędzie 'Nieprawidłowy JSON'", + "icon-settings":"Ustawienia ikony", + "dialog-editor-settings":"Ustawienia edytora dialogu", + "use-custom-icon":"Użyj niestandardowej ikony", + "input-cell-icon":"Ikona do wyświetlenia przed komórką wejściową", + "value-conversion-settings":"Ustawienia konwersji wartości", + "get-value-settings":"Ustawienia pobierania wartości", + "use-get-value-function":"Użyj funkcji getValue", + "get-value-function":"Funkcja getValue", + "set-value-settings":"Ustawienia ustawiania wartości", + "use-set-value-function":"Użyj funkcji setValue", + "set-value-function":"Funkcja setValue", + "json-invalid":"Wartość JSON ma nieprawidłowy format", + "title":"Tytuł", + "cancel-button-label":"Etykieta przycisku 'Anuluj'" + }, + "invalid-qr-code-text":"Nieprawidłowy tekst wejściowy dla kodu QR. Wejście powinno być typu string.", + "qr-code":{ + "use-qr-code-text-function":"Użyj funkcji tekstowej kodu QR", + "qr-code-text-pattern":"Wzór tekstu kodu QR (np. '${entityName} | ${keyName} - jakiś tekst.')", + "qr-code-text-pattern-hint":"Wzór tekstu kodu QR używa wartości pierwszego znalezionego klucza w encjach w aliasie encji.", + "qr-code-text-pattern-required":"Wzór tekstu kodu QR jest wymagany.", + "qr-code-text-function":"Funkcja tekstu kodu QR" + }, + "label-widget":{ + "label-pattern":"Wzór", + "label-pattern-hint":"Wskazówka: na przykład 'Tekst ${keyName} jednostki.' lub ${#<index klucza>} jednostki'", + "label-pattern-required":"Wzór jest wymagany", + "label-position":"Położenie (Procentowo w stosunku do tła)", + "x-pos":"X", + "y-pos":"Y", + "background-color":"Kolor tła", + "font-settings":"Ustawienia czcionki", + "background-image":"Obraz tła", + "labels":"Etykiety", + "no-labels":"Brak skonfigurowanych etykiet", + "add-label":"Dodaj etykietę" + }, + "navigation":{ + "title":"Tytuł", + "navigation-path":"Ścieżka nawigacji", + "filter-type":"Typ filtru", + "filter-type-all":"Wszystkie elementy", + "filter-type-include":"Zawiera elementy", + "filter-type-exclude":"Wyklucza elementy", + "items":"Elementy", + "enter-urls-to-filter":"Wprowadź adresy URL do filtrowania..." + }, + "persistent-table":{ + "rpc-id":"ID RPC", + "message-type":"Typ wiadomości", + "method":"Metoda", + "params":"Parametry", + "created-time":"Czas utworzenia", + "expiration-time":"Czas wygaśnięcia", + "retries":"Próby", + "status":"Status", + "filter":"Filtr", + "refresh":"Odśwież", + "add":"Dodaj żądanie RPC", + "details":"Szczegóły", + "delete":"Usuń", + "delete-request-title":"Usuń trwałe żądanie RPC", + "delete-request-text":"Czy na pewno chcesz usunąć to żądanie?", + "details-title":"Szczegóły ID RPC: ", + "additional-info":"Dodatkowe informacje", + "response":"Odpowiedź", + "any-status":"Dowolny status", + "rpc-status-list":"Lista statusów RPC", + "no-request-prompt":"Brak żądań do wyświetlenia", + "send-request":"Wyślij żądanie", + "add-title":"Utwórz trwałe żądanie RPC", + "method-error":"Wymagana jest metoda.", + "timeout-error":"Minimalna wartość czasu oczekiwania to 5000 (5 sekund).", + "white-space-error":"Biała przestrzeń nie jest dozwolona.", + "rpc-status":{ + "QUEUED":"W KOLEJCE", + "SENT":"WYSŁANE", + "DELIVERED":"DOSTARCZONE", + "SUCCESSFUL":"UDANE", + "TIMEOUT":"PRZEKROCZONY LIMIT CZASU", + "EXPIRED":"WYGASŁE", + "FAILED":"NIEUDANE" + }, + "rpc-search-status-all":"WSZYSTKO", + "message-types":{ + "false":"Dwukierunkowe", + "true":"Jednokierunkowe" + }, + "general-settings":"Ustawienia ogólne", + "enable-filter":"Włącz filtr", + "enable-sticky-header":"Wyświetl nagłówek podczas przewijania", + "enable-sticky-action":"Wyświetl kolumnę akcji podczas przewijania", + "display-request-details":"Wyświetl szczegóły żądania", + "allow-send-request":"Zezwalaj na wysyłanie żądań RPC", + "allow-delete-request":"Zezwalaj na usuwanie żądań", + "columns-settings":"Ustawienia kolumn", + "display-columns":"Wyświetlane kolumny", + "column":"Kolumna", + "no-columns-found":"Nie znaleziono kolumn", + "no-columns-matching":"'{{column}}' nie znaleziono." + }, + "range-chart":{ + "chart":"Wykres", + "data-zoom":"Powiększenie danych", + "range-colors":"Kolory zakresu", + "out-of-range-color":"Kolor poza zakresem", + "fill-area":"Wypełnij obszar", + "range-chart-card-style":"Styl karty wykresu zakresu" + }, + "rpc":{ + "value-settings":"Ustawienia wartości", + "initial-value":"Początkowa wartość", + "retrieve-value-settings":"Ustawienia pobierania włączonej/wyłączonej wartości", + "retrieve-value-method":"Pobierz wartość za pomocą metody", + "retrieve-value-method-none":"Nie pobieraj", + "retrieve-value-method-rpc":"Wywołaj metodę pobierania wartości RPC", + "retrieve-value-method-attribute":"Subskrybuj atrybut", + "retrieve-value-method-timeseries":"Subskrybuj szereg czasowy", + "attribute-value-key":"Klucz atrybutu", + "timeseries-value-key":"Klucz szeregu czasowego", + "get-value-method":"Metoda RPC pobierz wartość", + "parse-value-function":"Funkcja analizy wartości", + "update-value-settings":"Ustawienia aktualizacji wartości", + "set-value-method":"Metoda RPC ustaw wartość", + "convert-value-function":"Funkcja konwersji wartości", + "rpc-settings":"Ustawienia RPC", + "request-timeout":"Limit czasu żądania RPC (ms)", + "persistent-rpc-settings":"Ustawienia trwałego RPC", + "request-persistent":"Trwałe żądanie RPC", + "persistent-polling-interval":"Interwał odpytywania (ms) w celu uzyskania odpowiedzi na trwałe polecenie RPC", + "common-settings":"Wspólne ustawienia", + "switch-title":"Tytuł przełącznika", + "show-on-off-labels":"Pokaż etykiety włącz/wyłącz", + "slide-toggle-label":"Etykieta przycisku przełącznika", + "label-position":"Pozycja etykiety", + "label-position-before":"Przed", + "label-position-after":"Po", + "slider-color":"Kolor suwaka", + "slider-color-primary":"Podstawowy", + "slider-color-accent":"Akcent", + "slider-color-warn":"Ostrzeżenie", + "button-style":"Styl przycisku", + "button-raised":"Podniesiony przycisk", + "button-primary":"Podstawowy kolor", + "button-background-color":"Kolor tła przycisku", + "button-text-color":"Kolor tekstu przycisku", + "widget-title":"Tytuł widżetu", + "button-label":"Etykieta przycisku", + "device-attribute-scope":"Zakres atrybutu urządzenia", + "server-attribute":"Atrybut serwera", + "shared-attribute":"Atrybut wspólny", + "device-attribute-parameters":"Parametry atrybutu urządzenia", + "is-one-way-command":"Jest to polecenie jednokierunkowe", + "rpc-method":"Metoda RPC", + "rpc-method-params":"Parametry metody RPC", + "show-rpc-error":"Pokaż błąd wykonania polecenia RPC", + "led-title":"Tytuł diody LED", + "led-color":"Kolor diody LED", + "check-status-settings":"Ustawienia sprawdzania statusu", + "perform-rpc-status-check":"Wykonaj sprawdzanie statusu urządzenia RPC", + "retrieve-led-status-value-method":"Pobierz stan diody LED za pomocą metody", + "led-status-value-attribute":"Atrybut urządzenia zawierający stan diody LED", + "led-status-value-timeseries":"Szereg czasowy urządzenia zawierający stan diody LED", + "check-status-method":"Metoda sprawdzania statusu urządzenia RPC", + "parse-led-status-value-function":"Funkcja analizy stanu diody LED", + "knob-title":"Tytuł pokrętła", + "min-value":"Minimalna wartość", + "max-value":"Maksymalna wartość" + }, + "maps":{ + "select-entity":"Wybierz jednostkę", + "select-entity-hint":"Wskazówka: po wybraniu kliknij na mapie, aby ustawić pozycję", + "tooltips":{ + "placeMarker":"Kliknij, aby umieścić jednostkę '{{entityName}}'", + "firstVertex":"Wielokąt dla '{{entityName}}': kliknij, aby umieścić pierwszy punkt", + "firstVertex-cut":"Kliknij, aby umieścić pierwszy punkt", + "continueLine":"Wielokąt dla '{{entityName}}': kliknij, aby kontynuować rysowanie", + "continueLine-cut":"Kliknij, aby kontynuować rysowanie", + "finishLine":"Kliknij na istniejący znacznik, aby zakończyć", + "finishPoly":"Wielokąt dla '{{entityName}}': kliknij na pierwszy znacznik, aby zakończyć i zapisać", + "finishPoly-cut":"Kliknij na pierwszy znacznik, aby zakończyć i zapisać", + "finishRect":"Wielokąt dla '{{entityName}}': kliknij, aby zakończyć i zapisać", + "startCircle":"Okrąg dla '{{entityName}}': kliknij, aby umieścić środek okręgu", + "finishCircle":"Okrąg dla '{{entityName}}': kliknij, aby zakończyć okrąg", + "placeCircleMarker":"Kliknij, aby umieścić znacznik okręgu" + }, + "actions":{ + "finish":"Zakończ", + "cancel":"Anuluj", + "removeLastVertex":"Usuń ostatni punkt" + }, + "buttonTitles":{ + "drawMarkerButton":"Umieść obiekt", + "drawPolyButton":"Utwórz wielokąt", + "drawLineButton":"Utwórz linię", + "drawCircleButton":"Utwórz okrąg", + "drawRectButton":"Utwórz prostokąt", + "editButton":"Tryb edycji", + "dragButton":"Tryb przeciągania", + "cutButton":"Odcinanie obszaru wielokąta", + "deleteButton":"Usuń", + "drawCircleMarkerButton":"Utwórz znacznik okręgu", + "rotateButton":"Obróć wielokąt" + }, + "map-provider-settings":"Ustawienia dostawcy map", + "map-provider":"Dostawca map", + "map-provider-google":"Google Maps", + "map-provider-openstreet":"OpenStreet Maps", + "map-provider-here":"HERE Maps", + "map-provider-image":"Mapa obrazu", + "map-provider-tencent":"Tencent Maps", + "openstreet-provider":"Dostawca mapy OpenStreet", + "openstreet-provider-mapnik":"OpenStreetMap.Mapnik (Domyślny)", + "openstreet-provider-hot":"OpenStreetMap.HOT", + "openstreet-provider-esri-street":"Esri.WorldStreetMap", + "openstreet-provider-esri-topo":"Esri.WorldTopoMap", + "openstreet-provider-esri-imagery":"Esri.WorldImagery", + "openstreet-provider-cartodb-positron":"CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter":"CartoDB.DarkMatter", + "use-custom-provider":"Użyj dostawcy niestandardowego", + "custom-provider-tile-url":"Niestandardowy adres URL kafelka dostawcy", + "google-maps-api-key":"Klucz API Google Maps", + "default-map-type":"Domyślny rodzaj mapy", + "google-map-type-roadmap":"Mapa drogowa", + "google-map-type-satelite":"Satelita", + "google-map-type-hybrid":"Hybrydowa", + "google-map-type-terrain":"Tereno", + "map-layer":"Warstwa mapy", + "here-map-normal-day":"HERE.normalDay (Domyślna)", + "here-map-normal-night":"HERE.normalNight", + "here-map-hybrid-day":"HERE.hybridDay", + "here-map-terrain-day":"HERE.terrainDay", + "credentials":"Poświadczenia", + "here-app-id":"ID aplikacji HERE", + "here-app-code":"Kod aplikacji HERE", + "here-api-key":"Klucz API HERE", + "here-use-new-version-api-3":"Użyj wersji API 3", + "tencent-maps-api-key":"Klucz API Tencent Maps", + "tencent-map-type-roadmap":"Mapa drogowa", + "tencent-map-type-satelite":"Satelita", + "tencent-map-type-hybrid":"Hybrydowa", + "image-map-background":"Tło mapy obrazu", + "image-map-background-from-entity-attribute":"Użyj tła mapy obrazu z atrybutu encji", + "image-url-source-entity-alias":"Źródło URL obrazu z aliasu encji", + "image-url-source-entity-attribute":"Źródło URL obrazu z atrybutu encji", + "common-map-settings":"Wspólne ustawienia mapy", + "x-pos-key-name":"Nazwa klucza X", + "y-pos-key-name":"Nazwa klucza Y", + "latitude-key-name":"Nazwa klucza szerokości geograficznej", + "longitude-key-name":"Nazwa klucza długości geograficznej", + "default-map-zoom-level":"Domyślny poziom przybliżenia mapy (0 - 20)", + "default-map-center-position":"Domyślna pozycja centralna mapy (0,0)", + "disable-scroll-zooming":"Wyłącz przybliżanie za pomocą przewijania", + "disable-double-click-zooming":"Wyłącz przybliżanie podwójnym kliknięciem", + "disable-zoom-control-buttons":"Wyłącz przyciski sterowania przybliżeniem", + "fit-map-bounds":"Dopasuj granice mapy, aby pokryć wszystkie markery", + "use-default-map-center-position":"Użyj domyślnej pozycji centralnej mapy", + "entities-limit":"Limit encji do wczytania", + "markers-settings":"Ustawienia znaczników", + "marker-offset-x":"Przesunięcie X znacznika względem pozycji pomnożone przez szerokość znacznika", + "marker-offset-y":"Przesunięcie Y znacznika względem pozycji pomnożone przez wysokość znacznika", + "position-function":"Funkcja konwersji pozycji, powinna zwracać współrzędne x, y jako liczby z zakresu od 0 do 1", + "draggable-marker":"Znacznik do przeciągania", + "label":"Etykieta", + "show-label":"Pokaż etykietę", + "use-label-function":"Użyj funkcji etykiety", + "label-pattern":"Etykieta (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", + "label-function":"Funkcja etykiety", + "tooltip":"Podpowiedź", + "show-tooltip":"Pokaż podpowiedź", + "show-tooltip-action":"Działanie wyświetlania podpowiedzi", + "show-tooltip-action-click":"Pokaż podpowiedź po kliknięciu (Domyślnie)", + "show-tooltip-action-hover":"Pokaż podpowiedź po najechaniu", + "auto-close-tooltips":"Automatyczne zamykanie podpowiedzi", + "use-tooltip-function":"Użyj funkcji podpowiedzi", + "tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", + "tooltip-function":"Funkcja podpowiedzi", + "tooltip-offset-x":"Przesunięcie X podpowiedzi względem kotwicy znacznika pomnożone przez szerokość znacznika", + "tooltip-offset-y":"Przesunięcie Y podpowiedzi względem kotwicy znacznika pomnożone przez wysokość znacznika", + "color":"Kolor", + "use-color-function":"Użyj funkcji koloru", + "color-function":"Funkcja koloru", + "marker-image":"Obraz znacznika", + "use-marker-image-function":"Użyj funkcji obrazu znacznika", + "custom-marker-image":"Niestandardowy obraz znacznika", + "custom-marker-image-size":"Niestandardowy rozmiar obrazu znacznika (px)", + "marker-image-function":"Funkcja obrazu znacznika", + "marker-images":"Obrazy znaczników", + "polygon-settings":"Ustawienia wielokąta", + "show-polygon":"Pokaż wielokąt", + "polygon-key-name":"Nazwa klucza wielokąta", + "enable-polygon-edit":"Włącz edycję wielokąta", + "polygon-label":"Etykieta wielokąta", + "show-polygon-label":"Pokaż etykietę wielokąta", + "use-polygon-label-function":"Użyj funkcji etykiety wielokąta", + "polygon-label-pattern":"Etykieta wielokąta (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", + "polygon-label-function":"Funkcja etykiety wielokąta", + "polygon-tooltip":"Podpowiedź wielokąta", + "show-polygon-tooltip":"Pokaż podpowiedź wielokąta", + "auto-close-polygon-tooltips":"Automatyczne zamykanie podpowiedzi wielokąta", + "use-polygon-tooltip-function":"Użyj funkcji podpowiedzi wielokąta", + "polygon-tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", + "polygon-tooltip-function":"Funkcja podpowiedzi wielokąta", + "polygon-color":"Kolor wielokąta", + "polygon-opacity":"Przezroczystość wielokąta", + "use-polygon-color-function":"Użyj funkcji koloru wielokąta", + "polygon-color-function":"Funkcja koloru wielokąta", + "polygon-stroke":"Obrys wielokąta", + "stroke-color":"Kolor obrysu", + "stroke-opacity":"Przezroczystość obrysu", + "stroke-weight":"Grubość obrysu", + "use-polygon-stroke-color-function":"Użyj funkcji koloru obrysu wielokąta", + "polygon-stroke-color-function":"Funkcja koloru obrysu wielokąta", + "circle-settings":"Ustawienia okręgu", + "show-circle":"Pokaż okrąg", + "circle-key-name":"Nazwa klucza okręgu", + "enable-circle-edit":"Włącz edycję okręgu", + "circle-label":"Etykieta okręgu", + "show-circle-label":"Pokaż etykietę okręgu", + "use-circle-label-function":"Użyj funkcji etykiety okręgu", + "circle-label-pattern":"Etykieta okręgu (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", + "circle-label-function":"Funkcja etykiety okręgu", + "circle-tooltip":"Podpowiedź okręgu", + "show-circle-tooltip":"Pokaż podpowiedź okręgu", + "auto-close-circle-tooltips":"Automatyczne zamykanie podpowiedzi okręgu", + "use-circle-tooltip-function":"Użyj funkcji podpowiedzi okręgu", + "circle-tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", + "circle-tooltip-function":"Funkcja podpowiedzi okręgu", + "circle-fill-color":"Kolor wypełnienia okręgu", + "circle-fill-color-opacity":"Przezroczystość koloru wypełnienia okręgu", + "use-circle-fill-color-function":"Użyj funkcji koloru wypełnienia okręgu", + "circle-fill-color-function":"Funkcja koloru wypełnienia okręgu", + "circle-stroke":"Obrys okręgu", + "use-circle-stroke-color-function":"Użyj funkcji koloru obrysu okręgu", + "circle-stroke-color-function":"Funkcja koloru obrysu okręgu", + "markers-clustering-settings":"Ustawienia grupowania znaczników", + "use-map-markers-clustering":"Użyj grupowania znaczników na mapie", + "zoom-on-cluster-click":"Powiększ po kliknięciu na klastrze", + "max-cluster-zoom":"Maksymalny poziom powiększenia, przy którym znacznik może być częścią klastra (0 - 18)", + "max-cluster-radius-pixels":"Maksymalny promień, który może obejmować klaster w pikselach", + "cluster-zoom-animation":"Pokaż animację znaczników podczas przybliżania", + "show-markers-bounds-on-cluster-mouse-over":"Pokaż granice znaczników po najechaniu myszą na klaster", + "spiderfy-max-zoom-level":"Rozpoznaj na najwyższym poziomie powiększenia (aby zobaczyć wszystkie znaczniki klastra)", + "load-optimization":"Optymalizacja ładowania", + "cluster-chunked-loading":"Użyj fragmentów do dodawania znaczników, aby strona się nie zawiesiła", + "cluster-markers-lazy-load":"Użyj ładowania opóźnionego do dodawania znaczników", + "editor-settings":"Ustawienia edytora", + "enable-snapping":"Włącz przyciąganie do innych wierzchołków dla precyzyjnego rysowania", + "init-draggable-mode":"Inicjalizuj mapę w trybie przeciągania", + "hide-all-edit-buttons":"Ukryj wszystkie przyciski edycji", + "hide-draw-buttons":"Ukryj przyciski rysowania", + "hide-edit-buttons":"Ukryj przyciski edycji", + "hide-remove-button":"Ukryj przycisk usuwania", + "route-map-settings":"Ustawienia mapy trasy", + "trip-animation-settings":"Ustawienia animacji podróży", + "normalization-step":"Krok normalizacji danych (ms)", + "tooltip-background-color":"Kolor tła podpowiedzi", + "tooltip-font-color":"Kolor czcionki podpowiedzi", + "tooltip-opacity":"Przezroczystość podpowiedzi (0-1)", + "auto-close-tooltip":"Automatyczne zamykanie podpowiedzi", + "rotation-angle":"Ustaw dodatkowy kąt obrotu dla znacznika (stopnie)", + "path-settings":"Ustawienia ścieżki", + "path-color":"Kolor ścieżki", + "use-path-color-function":"Użyj funkcji koloru ścieżki", + "path-color-function":"Funkcja koloru ścieżki", + "path-decorator":"Dekorator ścieżki", + "use-path-decorator":"Użyj dekoratora ścieżki", + "decorator-symbol":"Symbol dekoratora", + "decorator-symbol-arrow-head":"Strzała", + "decorator-symbol-dash":"Kreska", + "decorator-symbol-size":"Rozmiar symbolu dekoratora (px)", + "use-path-decorator-custom-color":"Użyj niestandardowego koloru dekoratora ścieżki", + "decorator-custom-color":"Niestandardowy kolor dekoratora", + "decorator-offset":"Przesunięcie dekoratora", + "end-decorator-offset":"Przesunięcie końcowe dekoratora", + "decorator-repeat":"Powtórzenie dekoratora", + "points-settings":"Ustawienia punktów", + "show-points":"Pokaż punkty", + "point-color":"Kolor punktu", + "point-size":"Rozmiar punktu (px)", + "use-point-color-function":"Użyj funkcji koloru punktu", + "point-color-function":"Funkcja koloru punktu", + "use-point-as-anchor":"Użyj punktu jako kotwicy", + "point-as-anchor-function":"Funkcja punktu jako kotwicy", + "independent-point-tooltip":"Niezależna podpowiedź punktu", + "clustering-markers":"Klastrowanie znaczników", + "use-icon-create-function":"Użyj funkcji koloru znaczników", + "marker-color-function":"Funkcja koloru znacznika" + }, + "markdown":{ + "use-markdown-text-function":"Użyj funkcji wartości markdown/HTML", + "markdown-text-function":"Funkcja wartości markdown/HTML", + "markdown-text-pattern":"Wzorzec markdown/HTML (markdown lub HTML z zmiennymi, np. '${entityName} lub ${keyName} - jakiś tekst.')", + "apply-default-markdown-style":"Zastosuj domyślny styl markdown", + "markdown-css":"CSS markdown/HTML" + }, + "simple-card":{ + "label":"Etykieta", + "label-position":"Pozycja etykiety", + "label-position-left":"Po lewej", + "label-position-top":"Na górze" + }, + "value-card":{ + "layout":"Układ", + "layout-square":"Kwadratowy", + "layout-vertical":"Pionowy", + "layout-centered":"Wyśrodkowany", + "layout-simplified":"Uproszczony", + "layout-horizontal":"Poziomy", + "layout-horizontal-reversed":"Odwrócony poziomy", + "label":"Etykieta", + "icon":"Ikona", + "value":"Wartość", + "date":"Data", + "value-card-style":"Styl karty wartości", + "auto-scale":"Automatyczne dostosowanie skali" + }, + "liquid-level-card":{ + "layout-simple":"Prosty", + "layout-percentage":"Procentowy", + "layout-absolute":"Bezwzględny", + "layout":"Układ", + "background-overlay":"Nakładka na tło wartości", + "total-volume":"Całkowita objętość", + "tank":"Zbiornik", + "shape":"Kształt", + "datasource-units":"Jednostki źródła danych", + "widget-units":"Jednostki widżetu", + "decimals":"Miejsca dziesiętne", + "liquid":"Ciecz", + "liquid-color":"Kolor cieczy", + "value":"Wartość", + "value-font":"Czcionka wartości", + "level":"Poziom", + "last-update":"Ostatnia aktualizacja", + "shape-by-attribute":"Ustaw kształt zbiornika według nazwy atrybutu", + "tooltip-background":"Kolor tła", + "background-blur":"Rozmycie tła", + "tank-color":"Kolor zbiornika", + "static":"Statyczny", + "see-examples":"Zobacz przykłady", + "attribute":"Atrybut", + "shape-type":"Typ", + "v-oval":"Pionowy Owal", + "v-cylinder":"Pionowy Cylinder", + "v-capsule":"Pionowa Kapsuła", + "rectangle":"Prostokąt", + "h-oval":"Poziomy Owal", + "h-ellipse":"Pozioma Elipsa", + "h-dish-ends":"Poziome Zakończenia Dzbanka", + "h-cylinder":"Poziomy Cylinder", + "h-capsule":"Pozioma Kapsuła", + "h-elliptical_2_1":"Pozioma Elipsa 2:1", + "icon":"Ikona karty", + "title":"Tytuł karty", + "units":"Jednostki", + "color-and-font":"Kolor i czcionka", + "shape-attribute-name":"Nazwa atrybutu", + "total-volume-required":"Wymagana jest całkowita objętość.", + "attribute-name-required":"Wymagana jest nazwa atrybutu.", + "attribute-key-not-set":"Klucz atrybutu '{{attributeName}}' nie jest ustawiony", + "attribute-key-invalid":"Klucz atrybutu '{{attributeName}}' jest nieprawidłowy" + }, + "aggregated-value-card":{ + "subtitle":"Podtytuł", + "chart":"Wykres", + "values":"Wartości", + "value-appearance":"Wygląd wartości", + "position":"Pozycja", + "position-center":"Środek", + "position-right-top":"Prawy górny róg", + "position-right-bottom":"Prawy dolny róg", + "position-left-top":"Lewy górny róg", + "position-left-bottom":"Lewy dolny róg", + "font":"Czcionka", + "color":"Kolor", + "arrow":"Strzałka", + "display-up-down-arrow":"Wyświetl strzałkę w górę/dół", + "add-value":"Dodaj wartość", + "remove-value":"Usuń wartość", + "no-values":"Brak skonfigurowanych wartości", + "aggregation":"Agregacja", + "aggregated-value-card-style":"Styl karty zagregowanej wartości", + "auto-scale":"Automatyczne skalowanie" + }, + "value-chart-card":{ + "layout":"Układ", + "layout-left":"Lewo", + "layout-right":"Prawo", + "auto-scale":"Automatyczna skala", + "icon":"Ikona", + "value":"Wartość", + "chart":"Wykres", + "value-chart-card-style":"Styl karty wartości z wykresem" + }, + "progress-bar":{ + "layout":"Układ", + "layout-default":"Domyślny", + "layout-simplified":"Uproszczony", + "auto-scale":"Automatyczna skala", + "icon":"Ikona", + "value":"Wartość", + "range":"Zakres", + "min":"min", + "max":"max", + "range-ticks":"Podziałka zakresu", + "bar":"Pasek", + "bar-color":"Kolor paska", + "bar-background":"Tło paska", + "progress-bar-card-style":"Styl karty paska postępu" + }, + "alarm-count":{ + "alarm-count-card-style":"Styl karty licznika alarmów" + }, + "entity-count":{ + "entity-count-card-style":"Entity count card style" + }, + "count":{ + "layout":"Układ", + "layout-column":"Kolumna", + "layout-row":"Wiersz", + "label":"Etykieta", + "icon":"Ikona", + "icon-background":"Tło ikony", + "value":"Wartość", + "chevron":"Strzałka", + "auto-scale":"Automatyczna skala" + }, + "table":{ + "common-table-settings":"Wspólne ustawienia tabeli", + "enable-search":"Włącz wyszukiwanie", + "enable-sticky-header":"Zawsze wyświetlaj nagłówek", + "enable-sticky-action":"Zawsze wyświetlaj kolumnę akcji", + "hidden-cell-button-display-mode":"Tryb wyświetlania ukrytego przycisku akcji w komórce", + "show-empty-space-hidden-action":"Pokaż pustą przestrzeń zamiast ukrytego przycisku akcji w komórce", + "dont-reserve-space-hidden-action":"Nie rezerwuj miejsca na ukryte przyciski akcji", + "display-timestamp":"Znak czasu", + "display-pagination":"Wyświetl paginację", + "default-page-size":"Domyślny rozmiar strony", + "use-entity-label-tab-name":"Użyj etykiety encji w nazwie karty", + "hide-empty-lines":"Ukryj puste wiersze", + "row-style":"Styl wiersza", + "use-row-style-function":"Użyj funkcji stylu wiersza", + "row-style-function":"Funkcja stylu wiersza", + "cell-style":"Styl komórki", + "use-cell-style-function":"Użyj funkcji stylu komórki", + "cell-style-function":"Funkcja stylu komórki", + "cell-content":"Zawartość komórki", + "use-cell-content-function":"Użyj funkcji zawartości komórki", + "cell-content-function":"Funkcja zawartości komórki", + "show-latest-data-column":"Pokaż kolumnę z najnowszymi danymi", + "latest-data-column-order":"Kolejność kolumny z najnowszymi danymi", + "entities-table-title":"Tytuł tabeli encji", + "enable-select-column-display":"Włącz wybór kolumn do wyświetlenia", + "display-entity-name":"Wyświetl kolumnę z nazwą encji", + "entity-name-column-title":"Tytuł kolumny z nazwą encji", + "display-entity-label":"Wyświetl kolumnę z etykietą encji", + "entity-label-column-title":"Tytuł kolumny z etykietą encji", + "display-entity-type":"Wyświetl kolumnę z typem encji", + "default-sort-order":"Domyślna kolejność sortowania", + "custom-title":"Niestandardowy tytuł nagłówka", + "column-width":"Szerokość kolumny (px lub %)", + "default-column-visibility":"Domyślna widoczność kolumny", + "column-visibility-visible":"Widoczna", + "column-visibility-hidden":"Ukryta", + "column-visibility-hidden-mobile":"Ukryta w trybie mobilnym", + "column-selection-to-display":"Wybór kolumn w 'Kolumny do wyświetlenia'", + "column-selection-to-display-enabled":"Włączony", + "column-selection-to-display-disabled":"Wyłączony", + "alarms-table-title":"Tytuł tabeli alarmów", + "enable-alarms-selection":"Włącz wybór alarmów", + "enable-alarms-search":"Włącz wyszukiwanie alarmów", + "enable-alarm-filter":"Włącz filtr alarmów", + "display-alarm-details":"Wyświetl szczegóły alarmu", + "allow-alarms-ack":"Zezwalaj na potwierdzanie alarmów", + "allow-alarms-clear":"Zezwalaj na usuwanie alarmów", + "display-alarm-activity":"Wyświetl aktywność alarmu", + "allow-alarms-assign":"Zezwalaj na przypisywanie alarmów", + "columns":"Kolumny", + "column-settings":"Ustawienia kolumn", + "remove-column":"Usuń kolumnę", + "add-column":"Dodaj kolumnę", + "no-columns":"Brak skonfigurowanych kolumn", + "columns-to-display":"Kolumny do wyświetlenia", + "table-header":"Nagłówek tabeli", + "header-buttons":"Przyciski nagłówka", + "table-buttons":"Przyciski tabeli", + "pagination":"Paginacja", + "rows":"Wiersze", + "timeseries-column-error":"Należy określić co najmniej jedną kolumnę szeregów czasowych", + "alarm-column-error":"Należy określić co najmniej jedną kolumnę alarmów", + "table-tabs":"Karty tabeli", + "show-cell-actions-menu-mobile":"Pokaż menu rozwijane akcji komórek w trybie mobilnym" + }, + "wind-speed-direction":{ + "layout":"Układ", + "layout-default":"Domyślny", + "layout-advanced":"Zaawansowany", + "layout-simplified":"Uproszczony", + "values":"Wartości", + "wind-direction":"Kierunek wiatru", + "center-value":"Wartość środkowa", + "icon":"Ikona", + "arrow":"Strzałka", + "ticks":"Kreski", + "labels-type":"Typ etykiet", + "directional-names":"Nazwy kierunkowe", + "degrees":"Stopnie", + "major-ticks":"Główne kreski", + "minor-ticks":"Drobne kreski", + "wind-speed-direction-card-style":"Styl karty prędkości i kierunku wiatru", + "ticks-color":"Kolor kresek", + "ticks-labels-type":"Typ etykiet kreskowych", + "arrow-color":"Kolor strzałki" + }, + "value-source":{ + "value-source":"Źródło wartości", + "predefined-value":"Wartość predefiniowana", + "entity-attribute":"Atrybut jednostki", + "value":"Wartość", + "source-entity-alias":"Alias jednostki źródłowej", + "source-entity-attribute":"Atrybut jednostki źródłowej" + }, + "widget-font":{ + "font-settings":"Ustawienia czcionki", + "font-family":"Rodzina czcionek", + "size":"Rozmiar", + "relative-font-size":"Względny rozmiar czcionki (procenty)", + "font-style":"Styl", + "font-style-normal":"Normalny", + "font-style-italic":"Kursywa", + "font-style-oblique":"Pochyły", + "font-weight":"Grubość", + "font-weight-normal":"Normalna", + "font-weight-bold":"Pogrubiona", + "font-weight-bolder":"Grubsza", + "font-weight-lighter":"Lżejsza", + "color":"Kolor", + "shadow-color":"Kolor cienia", + "preview":"Podgląd", + "line-height":"Wysokość wiersza", + "auto":"Automatyczne" + }, + "home":{ + "no-data-available":"Brak dostępnych danych" + }, + "system-info":{ + "cpu":"Procesor", + "ram":"RAM", + "disk":"Dysk", + "cpu-warning-text":"Wysokie zużycie procesora. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu.", + "cpu-critical-text":"Krytycznie wysokie zużycie procesora. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu.", + "ram-warning-text":"Niski zapas pamięci RAM. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu lub zwiększ rozmiar pamięci RAM.", + "ram-critical-text":"Krytycznie niski zapas pamięci RAM. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu lub zwiększ rozmiar pamięci RAM.", + "disk-warning-text":"Niskie miejsce na dysku. Aby uniknąć utraty danych, zwolnij miejsce lub zwiększ rozmiar dysku.", + "disk-critical-text":"Krytycznie niskie miejsce na dysku. Aby uniknąć utraty danych, zwolnij miejsce lub zwiększ rozmiar dysku." + }, + "cluster-info":{ + "service-id":"ID usługi", + "service-type":"Typ usługi", + "no-data":"Brak danych" + }, + "transport-messages":{ + "title":"Wiadomości transportowe", + "info":"Wszystkie wiadomości odebrane od urządzeń" + }, + "activity":{ + "title":"Aktywność" + }, + "documentation":{ + "title":"Dokumentacja", + "add-link":"Dodaj link", + "add-link-title":"Dodaj link do dokumentacji", + "name":"Nazwa", + "name-required":"Nazwa jest wymagana.", + "link":"Link", + "link-required":"Link jest wymagany.", + "columns":"Kolumny" + }, + "quick-links":{ + "title":"Szybkie linki", + "add-link":"Dodaj link", + "add-link-title":"Dodaj szybki link", + "quick-link":"Szybki link", + "quick-link-required":"Szybki link jest wymagany.", + "no-links-matching":"Nie znaleziono linków pasujących do '{{name}}'.", + "columns":"Kolumny" + }, + "recent-dashboards":{ + "title":"Dashboardy", + "last":"Ostatnio przeglądane", + "starred":"Ulubione", + "name":"Nazwa", + "last-viewed":"Ostatnio przeglądane", + "no-last-viewed-dashboards":"Nie oglądałeś jeszcze żadnych dashboardów" + }, + "configured-features":{ + "title":"Skonfigurowane funkcje", + "info":"Status funkcji wymagających konfiguracji", + "email-feature":"Email", + "sms-feature":"SMS", + "slack-feature":"Slack", + "oauth2-feature":"OAuth 2", + "2fa-feature":"2FA", + "feature-configured":"Funkcja jest skonfigurowana.\nKliknij, aby skonfigurować", + "feature-not-configured":"Funkcja nie jest skonfigurowana.\nKliknij, aby skonfigurować" + }, + "version-info":{ + "title":"Wersja", + "contact-us":"Skontaktuj się z nami", + "current-version":"Aktualna wersja", + "current":"Aktualna", + "available-version":"Dostępna wersja", + "available":"Dostępna", + "upgrade":"Aktualizuj", + "version-is-up-to-date":"Wersja jest aktualna" + }, + "usage-info":{ + "title":"Użycie", + "entities":"Encje", + "api-calls":"Wywołania API" + }, + "functions":{ + "title":"Funkcje", + "pe-feature-tooltip":"Tylko w ThingsBoard\nProfessional Edition", + "switch-to-pe":"Przełącz do PE", + "alarms":"Alarmy", + "dashboards":"Dashboards", + "entities-and-relations":"Encje i relacje", + "profiles":"Profile", + "advanced-features":"Zaawansowane funkcje", + "notification-center":"Centrum powiadomień", + "api-usage":"Użycie API", + "customers":"Klienci", + "customers-hierarchy":"Hierarchia klientów", + "roles-and-permissions":"Role i uprawnienia", + "groups":"Grupy", + "integrations":"Integracje", + "solution-templates":"Szablony rozwiązań", + "scheduler":"Harmonogram", + "white-labeling":"White-labeling" + }, + "devices":{ + "view-docs":"Zobacz dokumentację", + "inactive":"Nieaktywne", + "active":"Aktywne", + "total":"Razem" + }, + "alarms":{ + "critical":"Krytyczne", + "assigned-to-me":"Przypisane do mnie", + "total":"Razem" + }, + "getting-started":{ + "get-started":"Rozpocznij", + "finish":"Zakończ", + "done-welcome-title":"Witamy na pokładzie", + "done-welcome-text":"Świetnie sobie poradziłeś!", + "sys-admin":{ + "step1":{ + "title":"Utwórz Najemcę i Administratora Najemcy", + "content":"

Najemca to osoba lub organizacja, która posiada lub wytwarza urządzenia i zasoby. Najemca może mieć wielu administratorów najemców, klientów, urządzeń i zasobów.

Administrator najemcy może tworzyć i zarządzać urządzeniami, zasobami, klientami i pulpitami w ramach konta najemcy.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-create-tenant":"Jak utworzyć Najemcę i Administratora Najemcy" + }, + "step2":{ + "title":"Skonfiguruj funkcję: Serwer poczty", + "content":"

Konfiguracja serwera poczty jest niezbędna do aktywacji użytkowników, odzyskiwania hasła i dostarczania powiadomień o alarmach.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-configure-mail-server":"Jak skonfigurować serwer poczty" + }, + "step3":{ + "title":"Skonfiguruj funkcję: Dostawca SMS", + "content":"

Skonfiguruj dostawców SMS, aby informować klientów o alarmach za pomocą wiadomości SMS.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-configure-sms-provider":"Jak skonfigurować dostawcę SMS" + }, + "step4":{ + "title":"Skonfiguruj funkcję: 2FA", + "content":"

Popraw bezpieczeństwo kont platformy za pomocą uwierzytelniania dwuetapowego.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-configure-2fa":"Jak skonfigurować 2FA" + }, + "step5":{ + "title":"Skonfiguruj funkcję: OAuth 2", + "content":"

Uprość logowanie dla użytkowników najemcy i klientów za pomocą funkcji jednokrotnego logowania za pośrednictwem OAuth 2.0.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-configure-oauth2":"Jak skonfigurować OAuth 2" + }, + "step6":{ + "title":"Skonfiguruj funkcję: Slack", + "content":"

Rozprowadzaj powiadomienia do użytkowników najemców i klientów za pośrednictwem Slack zgodnie z zasadami powiadomień, które ustawisz.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", + "how-to-configure-notifications":"Jak skonfigurować Slack" + } + }, + "tenant-admin":{ + "step1":{ + "title":"Utwórz urządzenie", + "content":"

Dodaj swoje pierwsze urządzenie do platformy za pomocą interfejsu użytkownika. Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-device":"Jak utworzyć urządzenie" + }, + "step2":{ + "title":"Podłącz urządzenie", + "content-before":"

Aby podłączyć urządzenie, potrzebujesz danych uwierzytelniających urządzenia. Zalecamy korzystanie z domyślnie generowanych danych uwierzytelniających, czyli token dostępu, zgodnie z tym przewodnikiem.

  • Przejdź do tabeli urządzeń
  • Kliknij w wiersz urządzenia, aby otworzyć szczegóły urządzenia
  • Naciśnij przycisk „Kopiuj token dostępu”

Użyj prostych poleceń, aby publikować dane za pomocą protokołu HTTP. Nie zapomnij zamienić $ACCESS_TOKEN na swój token dostępu do urządzenia:

", + "ubuntu":{ + "install-curl":"Zainstaluj cURL dla Ubuntu:" + }, + "macos":{ + "install-curl":"Zainstaluj cURL dla MacOS:" + }, + "windows":{ + "install-curl":"Od Windows 10 b17063 cURL jest dostępny domyślnie." + }, + "replace-access-token":"Zamień $ACCESS_TOKEN na token dostępu do swojego urządzenia:", + "content-after":"

Możesz także używać innych protokołów, takich jak MQTT, CoAP, itp.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-connect-device":"Jak podłączyć urządzenie" + }, + "step3":{ + "title":"Utwórz pulpit", + "content":"

Utwórz pulpit, aby wizualizować dane z jednostek, takich jak zasoby, urządzenia, itp.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-dashboard":"Jak utworzyć pulpit" + }, + "step4":{ + "title":"Skonfiguruj reguły alarmowe", + "alarm-rules":"Reguły alarmowe", + "content":"

Podnieśmy alarm, gdy temperatura osiągnie 25°C. Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-alarm-rules":"Jak skonfigurować reguły alarmowe" + }, + "step5":{ + "title":"Utwórz alarm", + "content-before":"

Aby uruchomić alarm, prześlij nową wartość telemetrii równej lub wyższej niż 26°C.

", + "replace-access-token":"Zamień $ACCESS_TOKEN na token dostępu do swojego urządzenia:", + "content-after":"

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-alarm":"Jak utworzyć alarm" + }, + "step6":{ + "title":"Utwórz klienta i przypisz pulpit", + "content":"

Tworząc pulpity dla końcowych użytkowników, użytkownik klienta może widzieć tylko swoje urządzenia, a dane innego klienta będą ukryte.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-customer-and-assign-dashboard":"Jak utworzyć klienta i przypisać pulpit" + } + } + } + }, + "color":{ + "color":"Kolor" + }, + "icon":{ + "icon":"Ikona", + "icons":"Ikony", + "select-icon":"Wybierz ikonę", + "material-icons":"Ikony materiałowe", + "show-all":"Pokaż wszystkie ikony", + "search-icon":"Wyszukaj ikonę", + "no-icons-found":"Nie znaleziono ikon dla '{{iconSearch}}'" + }, + "phone-input":{ + "phone-input-label":"Numer telefonu", + "phone-input-required":"Numer telefonu jest wymagany", + "phone-input-validation":"Numer telefonu jest nieprawidłowy lub niemożliwy", + "phone-input-pattern":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. {{phoneNumber}}", + "phone-input-hint":"Numer telefonu w formacie E.164, np. {{phoneNumber}}" + }, + "custom":{ + "widget-action":{ + "action-cell-button":"Przycisk w komórce akcji", + "row-click":"Po kliknięciu w wiersz", + "polygon-click":"Po kliknięciu w wielokąt", + "marker-click":"Po kliknięciu w marker", + "circle-click":"Po kliknięciu w okrąg", + "tooltip-tag-action":"Akcja etykiety narzędziowej", + "node-selected":"Po wybraniu węzła", + "element-click":"Po kliknięciu w element HTML", + "pie-slice-click":"Po kliknięciu w kawałek tarty", + "row-double-click":"Po podwójnym kliknięciu w wiersz", + "card-click":"Po kliknięciu w kartę" + } + }, + "paginator":{ + "items-per-page":"Elementy na stronie:", + "first-page-label":"Pierwsza strona", + "last-page-label":"Ostatnia strona", + "next-page-label":"Następna strona", + "previous-page-label":"Poprzednia strona", + "items-per-page-separator":"z" + }, + "language":{ + "language":"Język" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 5891208c4c..fdc0274cdf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -77,7 +77,10 @@ "show-more": "显示更多", "dont-show-again": "不再显示", "see-documentation": "查看文档", - "clear": "清除" + "clear": "清除", + "upload": "上传", + "delete-anyway": "仍要删除", + "delete-selected": "删除所选" }, "aggregation": { "aggregation": "聚合", @@ -470,7 +473,12 @@ "notifications-settings": "通知设置", "slack-api-token": "Slack API令牌", "slack": "Slack", - "slack-settings": "Slack 设置" + "slack-settings": "Slack 设置", + "maximum-password-length": "密码最大长度", + "maximum-password-length-min": "密码最大长度应至少为6个字符", + "maximum-password-length-less-min": "密码最大长度应大于最小长度", + "force-reset-password-if-no-valid": "如果密码不可用则强制重置密码", + "force-reset-password-if-no-valid-hint": "启用此功能时请小心:它会要求使用不可用密码的用户通过电子邮件重置其密码。" }, "alarm": { "alarm": "告警", @@ -598,7 +606,7 @@ "filter-type-single-entity": "单个实体", "filter-type-entity-list": "实体列表", "filter-type-entity-name": "实体名称", - "filter-type-entity-type": "Entity type", + "filter-type-entity-type": "实体类型", "filter-type-state-entity": "仪表板状态实体", "filter-type-state-entity-description": "仪表板实体令牌状态参数", "filter-type-asset-type": "资产类型", @@ -1188,7 +1196,9 @@ "assign-dashboard-to-edge": "将仪表板分配给边缘", "assign-dashboard-to-edge-text": "请选择要分配给边缘的仪表板", "non-existent-dashboard-state-error": "找不到ID为 '{{ stateId }}' 的仪表板状态。", - "edit-mode": "编辑模式" + "edit-mode": "编辑模式", + "state-controller-default": "静态(已弃用)", + "duplicate-state-action": "复制状态" }, "datakey": { "settings": "设置", @@ -1202,13 +1212,13 @@ "data-generation-func": "数据生成功能", "use-data-post-processing-func": "使用数据后处理功能", "configuration": "数据键配置", - "timeseries": "Timeseries", + "timeseries": "时间序列", "attributes": "属性", "entity-field": "实体字段", "alarm": "告警字段", - "timeseries-required": "实体 Timeseries 必填。", - "timeseries-or-attributes-required": "实体 Timeseries/属性必填。", - "alarm-fields-timeseries-or-attributes-required": "告警字段或实体 Timeseries/属性必填。", + "timeseries-required": "实体时间序列必填。", + "timeseries-or-attributes-required": "实体时间序列/属性必填。", + "alarm-fields-timeseries-or-attributes-required": "告警字段或实体时间序列/属性必填。", "maximum-timeseries-or-attributes": "最多允许 { count, plural, =1 {1 个 timeseries/属性。} other {# 个 timeseries/属性。} }", "alarm-fields-required": "告警字段必填。", "function-types": "函数类型", @@ -1226,10 +1236,10 @@ "latest-key": "最新值数据键", "latest-key-functions": "最新值数据键函数", "latest-key-function": "最新值数据键函数", - "timeseries-keys": "Timeseries 数据键", - "timeseries-key": "Timeseries 数据键", - "timeseries-key-functions": "Timeseries数据键函数", - "timeseries-key-function": "Timeseries数据键函数", + "timeseries-keys": "时间序列数据键", + "timeseries-key": "时间序列数据键", + "timeseries-key-functions": "时间序列数据键函数", + "timeseries-key-function": "时间序列数据键函数", "maximum-function-types": "最多允许 { count, plural, =1 {1 个函数类型} other {# 个函数类型} }", "time-description": "当前值的时间戳;", "value-description": "当前值;", @@ -1578,9 +1588,9 @@ "mqtt-send-ack-on-validation-exception": "发布消息验证失败时发送PUBACK", "mqtt-send-ack-on-validation-exception-hint": "默认情况下平台将关闭相关消息验证失败的MQTT会话,启用后平台将发布确认而不是关闭会话。", "snmp-add-mapping": "添加SNMP映射", - "snmp-mapping-not-configured": "OID到Timeseries/遥测的映射未配置", - "snmp-timseries-or-attribute-name": "用于映射的Timeseries/属性名称", - "snmp-timseries-or-attribute-type": "用于映射的Timeseries/属性类型", + "snmp-mapping-not-configured": "OID到时间序列/遥测的映射未配置", + "snmp-timseries-or-attribute-name": "用于映射的时间序列/属性名称", + "snmp-timseries-or-attribute-type": "用于映射的时间序列/属性类型", "snmp-method-pdu-type-get-request": "GetRequest", "snmp-method-pdu-type-get-next-request": "GetNextRequest", "snmp-oid": "OID", @@ -2017,7 +2027,8 @@ "sync-process-started-successfully": "同步处理开始成功!", "missing-related-rule-chains-title": "边缘缺少关联规则链", "missing-related-rule-chains-text": "分配给边缘的规则链使用规则节点将消息转发给未分配给当前边缘的规则链。

缺少的规则链列表:
{{missingRuleChains}}", - "widget-datasource-error": "组件只支持边缘实体数据源" + "widget-datasource-error": "组件只支持边缘实体数据源", + "upgrade-instructions": "升级说明" }, "edge-event": { "type-dashboard": "仪表板", @@ -2046,7 +2057,7 @@ "action-type-post-attributes": "推送属性", "action-type-attributes-updated": "属性更新", "action-type-attributes-deleted": "属性删除", - "action-type-timeseries-updated": "时序更新", + "action-type-timeseries-updated": "时间序列更新", "action-type-credentials-updated": "认证更新", "action-type-assigned-to-customer": "分配给客户", "action-type-unassigned-from-customer": "取消分配客户", @@ -2211,7 +2222,9 @@ "type-notification-request": "通知请求", "type-notification-template": "通知模板", "type-notification-templates": "通知模板", - "list-of-notification-templates": "{ count, plural, =1 {1 个通知模板} other {# 个通知模板} }" + "list-of-notification-templates": "{ count, plural, =1 {1 个通知模板} other {# 个通知模板} }", + "type-tb-resources": "资源", + "list-of-tb-resources": "{ count, plural, =1 {1 个资源} other {# 个资源} }" }, "entity-field": { "created-time": "创建时间", @@ -2328,7 +2341,7 @@ "attributes-propagation": "属性传播", "attributes-propagation-hint": "每次保存或更新这个实体视图时,实体视图将自动从目标实体复制指定的属性。由于性能原因,目标实体属性不会在每次属性更改时传递到实体视图。您可以通过配置\"copy to view\"规则链中的规则节点,并将\"Post attributes\"和\"attributes Updated\"消息链接到新规则节点,从而启用自动传递。", "timeseries-data": "时间序列数据", - "timeseries-data-hint": "配置目标实体的 Timeseries 数据键,以便实体视图可以访问这些键。此 Timeseries 数据是只读的。", + "timeseries-data-hint": "配置目标实体的时间序列数据键,以便实体视图可以访问这些键。此时间序列数据是只读的。", "search": "查找实体视图", "selected-entity-views": "已选择 { count, plural, =1 {1 个实体视图} other {# 个实体视图} }", "make-public-entity-view-title": "确定要将实体视图 '{{entityViewName}}' 设为公开吗?", @@ -2427,8 +2440,8 @@ "attributes": "属性", "add-attribute": "添加属性", "add-map": "添加映射元素", - "timeseries": "Timeseries", - "add-timeseries": "添加 Timeseries", + "timeseries": "时间序列", + "add-timeseries": "添加时间序列", "field-required": "必填字段", "brokers": "代理服务器组", "add-broker": "添加代理服务器", @@ -2523,7 +2536,7 @@ "modbus-device-name": "设备名称", "modbus-poll-period": "轮询周期 (毫秒)", "modbus-attributes-poll-period": "轮询属性周期 (毫秒)", - "modbus-timeseries-poll-period": "Timeseries 轮询周期 (毫秒)", + "modbus-timeseries-poll-period": "时间序列数据轮询周期 (毫秒)", "modbus-poll-period-range": "轮询周期应为正值。", "modbus-tag": "标签", "modbus-function": "函数", @@ -2613,7 +2626,7 @@ "key-type": { "key-type": "键类型", "attribute": "属性", - "timeseries": "Timeseries", + "timeseries": "时间序列", "entity-field": "实体", "constant": "常量", "client-attribute": "客户端属性", @@ -2908,7 +2921,13 @@ "grpc-max-pings-without-data": "在没有接收到任何数据之前,服务器可以发送的keepalive ping消息的最大数量,然后将连接视为死亡", "grpc-min-ping-interval-without-data": "在没有发送或接收数据时,服务器在发送keepalive ping消息之间应等待的最小时间量", "permit-without-calls": "允许服务器在没有活动RPC调用时保持GRPC连接活动" - } + }, + "docker-label": "使用以下指令在 Docker compose 中运行 IoT 网关,并为选定的设备提供凭据", + "install-docker-compose": "使用以下说明下载、安装和设置 Docker Compose", + "download-configuration-file": "下载配置文件", + "download-docker-compose": "下载您的网关的 docker-compose.yml 文件", + "launch-gateway": "启动网关", + "launch-docker-compose": "在包含 docker-compose.yml 文件的文件夹中,使用以下命令在终端中启动网关" }, "grid": { "delete-item-title": "确定要删除此项吗?", @@ -2939,6 +2958,59 @@ "browse-file": "浏览文件", "browse-files": "浏览文件" }, + "image": { + "gallery": "图像库", + "search": "搜索图像", + "selected-images": "已选择 { count, plural, =1 {1 个图像} other {# 个图像} }", + "created-time": "创建时间", + "name": "名称", + "name-required": "名称不能为空。", + "resolution": "分辨率", + "size": "大小", + "system": "系统", + "download-image": "下载图像", + "export-image": "导出图像为JSON", + "import-image": "从JSON导入图像", + "upload-image": "上传图像", + "edit-image": "编辑图像", + "image-details": "图像详情", + "no-images": "未找到图像", + "delete-image": "删除图像", + "delete-image-title": "确定要删除图像 '{{imageTitle}}' 吗?", + "delete-image-text": "请注意,确认后图像将无法恢复。", + "delete-images-title": "确定要删除 { count, plural, =1 {1 个图像} other {# 个图像} } 吗?", + "delete-images-text": "请注意,确认后所有选定的图像都将被删除,并且所有相关数据将无法恢复。", + "list-mode": "列表视图", + "grid-mode": "网格视图", + "image-preview": "图像预览", + "update-image": "更新图像", + "export-failed-error": "无法导出图像:{{error}}", + "image-json-file": "图像JSON文件", + "invalid-image-json-file-error": "无法从JSON导入图像:无效的图像JSON数据结构。", + "image-is-in-use": "图像被其他实体使用", + "images-are-in-use": "图像被其他实体使用", + "image-is-in-use-text": "无法删除图像'{{title}}',因为它被以下实体使用:", + "images-are-in-use-text": "由于图像被其他实体使用,无法删除所有图像。
您可以通过单击相应图像行上的引用按钮查看引用的实体。
如果仍然要删除这些图像,请在下方的表格中选择它们,然后点击删除所选按钮。", + "delete-image-in-use-text": "如果仍然要删除该图像,请点击无论如何删除按钮。", + "system-entities": "系统实体:", + "entities": "实体:", + "references": "引用", + "include-system-images": "包含系统图像", + "clear-image": "清除图像", + "no-image": "无图像", + "no-image-selected": "未选择图像", + "browse-from-gallery": "从图像库浏览", + "set-link": "设置链接", + "image-link": "图像链接", + "link": "链接", + "copy-image-link": "复制图像链接", + "embed-image": "嵌入图像", + "embed-to-html": "嵌入到HTML", + "embed-to-html-hint": "此功能将使链接对任何未经授权的用户可用。", + "embed-to-html-text": "使用以下代码片段,您可以将图像嵌入到基于纯HTML的组件中。
此类组件包括HTML卡片小部件、单元格内容函数等。", + "embed-to-angular-template": "嵌入到Angular HTML模板", + "embed-to-angular-template-text": "使用以下代码片段,您可以将图像嵌入到Angular HTML模板中。
此类组件包括Markdown小部件、小部件编辑器中的HTML部分、自定义操作等。" + }, "image-input": { "drop-images-or": "拖放一张或多张图片", "drag-and-drop": "拖放", @@ -2972,7 +3044,7 @@ "client-attribute": "客户端属性", "shared-attribute": "共享属性", "server-attribute": "服务器属性", - "timeseries": "Timeseries", + "timeseries": "时间序列", "entity-field": "实体字段", "access-token": "访问令牌", "x509": "X.509", @@ -3192,8 +3264,8 @@ "email-preview": "Email 通知预览", "slack": "Slack", "slack-preview": "Slack 通知预览", - "microsoft-teams" : "Microsoft Teams", - "microsoft-teams-preview" : "Microsoft Teams 通知预览", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams 通知预览", "sms": "SMS", "sms-preview": "SMS 通知预览", "web": "Web", @@ -3459,7 +3531,8 @@ "version-tag": "版本标签", "version-tag-hint": "自定义标签应与您设备报告的软件包版本相匹配。", "version-max-length": "版本长度应该少于256个字符", - "warning-after-save-no-edit": "上传包后,您将无法修改标题、版本、设备配置和包类型。" + "warning-after-save-no-edit": "上传包后,您将无法修改标题、版本、设备配置和包类型。", + "package-file": "包文件" }, "position": { "top": "顶部", @@ -3557,7 +3630,8 @@ "password-requirements": "密码要求", "password-should-difference": "新密码应与当前密码不同", "special-character": "{ count, plural, =1 {1 位特殊字符} other {# 位特殊字符} }", - "uppercase-letter": "{ count, plural, =1 {1 位大写字母} other {# 位大写字母} }" + "uppercase-letter": "{ count, plural, =1 {1 位大写字母} other {# 位大写字母} }", + "at-most": "最多:" } }, "relation": { @@ -3640,7 +3714,9 @@ "js-module": "JS 模块", "lwm2m-model": "LWM2M 模型", "pkcs-12": "PKCS #12" - } + }, + "resource-file": "资源文件", + "resource-files": "资源文件" }, "rulechain": { "rulechain": "规则链", @@ -4104,7 +4180,10 @@ "transport-device-telemetry-msg": "传输设备遥测消息", "transport-device-telemetry-data-points": "传输设备遥测数据点", "sec": "秒" - } + }, + "maximum-resource-size": "最大资源文件大小(字节)", + "maximum-resource-size-required": "最大资源文件大小是必需的", + "maximum-resource-size-range": "最大资源文件大小不能为负数" }, "timeinterval": { "seconds-interval": "{ seconds, plural, =1 {1 秒} other {# 秒} }", @@ -4205,409 +4284,6 @@ "background-color": "背景颜色", "background-blur": "背景模糊" }, - "unit": { - "millimeter": "mm", - "centimeter": "cm", - "angstrom": "Å", - "nanometer": "nm", - "micrometer": "μm", - "meter": "m", - "kilometer": "km", - "inch": "in", - "foot": "ft", - "yard": "yd", - "mile": "mi", - "nautical-mile": "nmi", - "astronomical-unit": "au", - "reciprocal-metre": "m⁻¹", - "meter-per-meter": "m/m", - "steradian": "sr", - "thou": "th", - "barleycorn": "bc", - "hand": "hd", - "chain": "ch", - "furlong": "fur", - "league": "lea", - "fathom": "fath", - "cable": "cb", - "link": "li", - "rod": "rd", - "nanogram": "ng", - "microgram": "μg", - "milligram": "mg", - "gram": "g", - "kilogram": "kg", - "tonne": "t", - "ounce": "oz", - "pound": "lb", - "stone": "st", - "hundredweight-count": "cwt", - "short-tons": "s.t.", - "dalton": "Da", - "grain": "gr", - "drachm": "dr", - "quarter": "qr", - "slug": "slug", - "carat": "ct", - "cubic-millimeter": "mm³", - "cubic-centimeter": "cm³", - "cubic-meter": "m³", - "cubic-kilometer": "km³", - "microliter": "μL", - "milliliter": "mL", - "liter": "L", - "hectoliter": "hL", - "cubic-inch": "in³", - "cubic-foot": "ft³", - "cubic-yard": "yd³", - "fluid-ounce": "fl oz", - "pint": "pt", - "quart": "qt", - "gallon": "gal", - "oil-barrels": "bbl", - "cubic-meter-per-kilogram": "m³/kg", - "gill": "gi", - "hogshead": "hhd", - "teaspoon": "tsp", - "tablespoon": "tbsp", - "cup": "cup", - "celsius": "°C", - "kelvin": "K", - "rankine": "°R", - "fahrenheit": "°F", - "percent": "%", - "meter-per-second": "m/s", - "kilometer-per-hour": "km/h", - "foot-per-second": "ft/s", - "mile-per-hour": "mph", - "knot": "kn", - "millimeters-per-minute": "mm/min", - "kilometer-per-hour-squared": "km/h²", - "foot-per-second-squared": "ft/s²", - "pascal": "Pa", - "kilopascal": "kPa", - "megapascal": "MPa", - "gigapascal": "GPa", - "millibar": "mbar", - "bar": "bar", - "kilobar": "kbar", - "newton": "N", - "newton-meter": "N·m", - "foot-pounds": "ft·lbf", - "inch-pounds": "in·lbf", - "newton-per-meter": "N/m", - "atmospheres": "atm", - "pounds-per-square-inch": "psi", - "torr": "Torr", - "inches-of-mercury": "inHg", - "pascal-per-square-meter": "Pa/m²", - "pound-per-square-inch": "psi", - "newton-per-square-meter": "N/m²", - "kilogram-force-per-square-meter": "kgf/m²", - "pascal-per-square-centimeter": "Pa/cm²", - "ton-force-per-square-inch": "tonf/in²", - "kilonewton-per-square-meter": "kN/m²", - "newton-per-square-millimeter": "N/mm²", - "microjoule": "μJ", - "millijoule": "mJ", - "joule": "J", - "kilojoule": "kJ", - "megajoule": "MJ", - "gigajoule": "GJ", - "watt-hour": "Wh", - "kilowatt-hour": "kWh", - "electron-volts": "eV", - "joules-per-coulomb": "J/C", - "british-thermal-unit": "BTU", - "foot-pound": "ft·lb", - "calorie": "cal", - "small-calorie": "cal", - "kilocalorie": "kcal", - "joule-per-kelvin": "J/K", - "joule-per-kilogram-kelvin": "J/(kg·K)", - "joule-per-kilogram": "J/kg", - "watt-per-meter-kelvin": "W/(m·K)", - "joule-per-cubic-meter": "J/m³", - "therm": "thm", - "electric-dipole-moment": "Debye", - "magnetic-dipole-moment": "Am²", - "debye": "D", - "coulomb-per-square-meter-per-volt": "C/(m²·V)", - "milliwatt": "mW", - "microwatt": "μW", - "watt": "W", - "kilowatt": "kW", - "megawatt": "MW", - "gigawatt": "GW", - "metric-horsepower": "PS", - "milliwatt-per-square-centimeter": "mW/cm²", - "watt-per-square-centimeter": "W/cm²", - "kilowatt-per-square-centimeter": "kW/cm²", - "milliwatt-per-square-meter": "mW/m²", - "watt-per-square-meter": "W/m²", - "kilowatt-per-square-meter": "kW/m²", - "watt-per-square-inch": "W/in²", - "kilowatt-per-square-inch": "kW/in²", - "horsepower": "hp", - "btu-per-hour": "BTU/h", - "coulomb": "C", - "millicoulomb": "mC", - "microcoulomb": "μC", - "picocoulomb": "pC", - "coulomb-per-meter": "C/m", - "coulomb-per-cubic-meter": "C/m³", - "coulomb-per-square-meter": "C/m²", - "square-millimeter": "mm²", - "square-centimeter": "cm²", - "square-meter": "m²", - "hectare": "ha", - "square-kilometer": "km²", - "square-inch": "in²", - "square-foot": "ft²", - "square-yard": "yd²", - "acre": "ac", - "square-mile": "mi²", - "are": "a", - "barn": "b", - "circular-inch": "c in²", - "milliampere-hour": "mAh", - "ampere-hours": "Ah", - "kiloampere-hours": "kAh", - "nanoampere": "nA", - "picoampere": "pA", - "microampere": "μA", - "milliampere": "mA", - "ampere": "A", - "kiloamperes": "kA", - "microampere-per-square-centimeter": "μA/cm²", - "ampere-per-square-meter": "A/m²", - "ampere-per-meter": "A/m", - "oersted": "Oe", - "bohr-magneton": "μB", - "ampere-meter-squared": "A·m²", - "ampere-meter": "A·m", - "nanovolt": "nV", - "picovolt": "pV", - "millivolts": "mV", - "microvolts": "μV", - "volt": "V", - "kilovolts": "kV", - "dbmV": "dBmV", - "dbm": "dBm", - "volt-meter": "V·m", - "kilovolt-meter": "kV·m", - "megavolt-meter": "MV·m", - "microvolt-meter": "μV·m", - "millivolt-meter": "mV·m", - "nanovolt-meter": "nV·m", - "ohm": "Ω", - "microohm": "μΩ", - "milliohm": "mΩ", - "kilohm": "kΩ", - "megohm": "MΩ", - "gigohm": "GΩ", - "hertz": "Hz", - "kilohertz": "kHz", - "megahertz": "MHz", - "gigahertz": "GHz", - "rpm": "rpm", - "candela-per-square-meter": "cd/m²", - "candela": "cd", - "lumen": "lm", - "lux": "lx", - "foot-candle": "fc", - "lumen-per-square-meter": "lm/m²", - "lux-second": "lx·s", - "lumen-second": "lm·s", - "lumens-per-watt": "lm/W", - "absorbance": "AU", - "mole": "mol", - "nanomole": "nmol", - "micromole": "μmol", - "millimole": "mmol", - "kilomole": "kmol", - "mole-per-cubic-meter": "mol/m³", - "rssi": "RSSI", - "ppm": "ppm", - "ppb": "ppb", - "micrograms-per-cubic-meter": "μg/m³", - "aqi": "AQI", - "gram-per-cubic-meter": "g/m³", - "gram-per-kilogram": "g/kg", - "millimeters-per-second": "mm/s", - "neper": "Np", - "bel": "B", - "decibel": "dB", - "meters-per-second-squared": "m/s²", - "becquerel": "Bq", - "curie": "Ci", - "gray": "Gy", - "sievert": "Sv", - "roentgen": "R", - "cps": "cps", - "rad": "rad", - "rem": "rem", - "dps": "dps", - "rutherford": "Rd", - "coulombs-per-kilogram": "C/kg", - "becquerels-per-cubic-meter": "Bq/m³", - "curies-per-liter": "Ci/L", - "becquerels-per-second": "Bq/s", - "curies-per-second": "Ci/s", - "gy-per-second": "Gy/s", - "watt-per-steradian": "W/sr", - "watt-per-square-metre-steradian": "W/(m²·sr)", - "ph-level": "pH", - "turbidity": "NTU", - "mg-per-liter": "mg/L", - "microsiemens-per-centimeter": "μS/cm", - "millisiemens-per-meter": "mS/m", - "siemens-per-meter": "S/m", - "kilogram-per-cubic-meter": "kg/m³", - "gram-per-cubic-centimeter": "g/cm³", - "kilogram-per-square-meter": "kg/m²", - "milligram-per-milliliter": "mg/mL", - "pound-per-cubic-foot": "lb/ft³", - "ounces-per-cubic-inch": "oz/in³", - "tons-per-cubic-yard": "ton/yd³", - "particle-density": "PD", - "kilometers-per-liter": "km/L", - "miles-per-gallon": "mpg", - "liters-per-100-km": "L/100 km", - "gallons-per-mile": "gal/mi", - "liters-per-hour": "L/h", - "gallons-per-hour": "gal/h", - "beats-per-minute": "bpm", - "millimeters-of-mercury": "mmHg", - "milligrams-per-deciliter": "mg/dL", - "g-force": "G", - "kilonewton": "kN", - "kilogram-force": "kgf", - "pound-force": "lbf", - "kilopound-force": "kip", - "dyne": "dyn", - "poundal": "pdl", - "kip": "kip", - "gal": "Gal", - "gravity": "g₀", - "hectopascal": "hPa", - "atmosphere": "atm", - "millibars": "mbar", - "inch-of-mercury": "inHg", - "richter-scale": "Richter", - "second": "s", - "minute": "min", - "hour": "hr", - "day": "day", - "week": "wk", - "month": "mo", - "year": "yr", - "cubic-foot-per-minute": "ft³/min", - "cubic-meters-per-hour": "m³/h", - "cubic-meters-per-second": "m³/s", - "liter-per-second": "L/s", - "liter-per-minute": "L/min", - "gallons-per-minute": "GPM", - "cubic-foot-per-second": "ft³/s", - "milliliters-per-minute": "mL/min", - "bit": "bit", - "byte": "B", - "kilobyte": "KB", - "megabyte": "MB", - "gigabyte": "GB", - "terabyte": "TB", - "petabyte": "PB", - "exabyte": "EB", - "zettabyte": "ZB", - "yottabyte": "YB", - "bit-per-second": "bps", - "kilobit-per-second": "Kbps", - "megabit-per-second": "Mbps", - "gigabit-per-second": "Gbps", - "terabit-per-second": "Tbps", - "byte-per-second": "B/s", - "kilobyte-per-second": "KB/s", - "megabyte-per-second": "MB/s", - "gigabyte-per-second": "GB/s", - "degree": "°", - "radian": "rad", - "gradian": "grad", - "mil": "mil", - "revolution": "rev", - "siemens": "S", - "millisiemens": "mS", - "microsiemens": "μS", - "kilosiemens": "kS", - "megasiemens": "MS", - "gigasiemens": "GS", - "farad": "F", - "millifarad": "mF", - "microfarad": "μF", - "nanofarad": "nF", - "picofarad": "pF", - "kilofarad": "kF", - "megafarad": "MF", - "gigafarad": "GF", - "terfarad": "TF", - "farad-per-meter": "F/m", - "tesla": "T", - "gauss": "G", - "kilogauss": "kG", - "millitesla": "mT", - "microtesla": "μT", - "nanotesla": "nT", - "kilotesla": "kT", - "megatesla": "MT", - "millitesla-square-meters": "mT·m²", - "gamma": "γ", - "lambda": "λ", - "square-meter-per-second": "m²/s", - "square-centimeter-per-second": "cm²/s", - "stoke": "St", - "centistokes": "cSt", - "square-foot-per-second": "ft²/s", - "square-inch-per-second": "in²/s", - "pascal-second": "Pa·s", - "centipoise": "cP", - "poise": "P", - "reynolds": "Re", - "pound-per-foot-hour": "lb/(ft·hr)", - "newton-second-per-square-meter": "N·s/m²", - "dyne-second-per-square-centimeter": "dyn·s/cm²", - "kilogram-per-meter-second": "kg/(m·s)", - "tesla-square-meters": "T·m²", - "maxwell": "Mx", - "tesla-per-meter": "T/m", - "gauss-per-centimeter": "G/cm", - "weber": "Wb", - "microweber": "μWb", - "milliweber": "mWb", - "gauss-square-centimeter": "G·cm²", - "kilogauss-square-centimeter": "kG·cm²", - "henry": "H", - "millihenry": "mH", - "microhenry": "μH", - "nanohenry": "nH", - "henry-per-meter": "H/m", - "tesla-meter-per-ampere": "T·m/A", - "gauss-per-oersted": "G/Oe", - "kilogram-per-mole": "kg/mol", - "gram-per-mole": "g/mol", - "milligram-per-mole": "mg/mol", - "joule-per-mole": "J/mol", - "joule-per-mole-kelvin": "J/mol-K", - "millivolts-per-meter": "mV/m", - "volts-per-meter": "V/m", - "kilovolts-per-meter": "kV/m", - "radian-per-second": "rad/s", - "radian-per-second-squared": "rad/s^2", - "revolutions-per-minute-per-second": "rpm/s", - "revolutions-per-minute-per-second-squared": "rpm/s^2", - "deg-per-second": "deg/s", - "degrees-brix": "°Bx", - "katal": "kat", - "katal-per-cubic-metre": "kat/m^3" - }, "user": { "user": "用户", "users": "用户", @@ -4765,15 +4441,15 @@ "no-widgets-text": "未找到部件", "management": "管理部件", "editor": "部件编辑器", - "confirm-to-exit-editor-html": "You have unsaved widget settings.
Are you sure you want to leave this page?", + "confirm-to-exit-editor-html": "有未保存的部件设置。
确定要离开此页面吗?", "widget-type-not-found": "加载部件配置出错。
可能关联的部件已经删除了。", "widget-type-load-error": "由于以下错误未加载部件:", "remove": "删除部件", - "delete": "Delete widget", + "delete": "删除部件", "edit": "编辑部件", "remove-widget-title": "确定要删除 '{{widgetTitle}}'部件吗?", "remove-widget-text": "确认后,控件和所有相关数据将变得不可恢复。", - "timeseries": "Timeseries", + "timeseries": "时间序列", "search-data": "查找数据", "no-data-found": "未找到数据", "latest": "最新值", @@ -5148,12 +4824,12 @@ "axis-position-top": "顶部 (默认)", "axis-position-bottom": "底部", "custom-legend-settings": "自定义图例设置", - "enable-custom-legend": "启用自定义图例 (这将允许您在键标签中使用属性/Timeseries值)", + "enable-custom-legend": "启用自定义图例 (这将允许您在键标签中使用属性/时间序列值)", "key-name": "键名", "key-name-required": "键名是必需的", "key-type": "键类型", "key-type-attribute": "属性", - "key-type-timeseries": "Timeseries", + "key-type-timeseries": "时间序列", "label-keys-list": "要在标签中使用的键列表", "no-label-keys": "未配置键", "add-label-key": "添加新键", @@ -5552,7 +5228,7 @@ "discard-changes": "放弃更改", "entity-attribute-required": "实体属性必填", "entity-coordinate-required": "纬度和经度两个字段都是必需的", - "entity-timeseries-required": "实体 Timeseries 必填", + "entity-timeseries-required": "实体时间序列必填", "get-location": "获取当前位置", "invalid-date": "无效日期", "latitude": "纬度", @@ -5570,19 +5246,19 @@ "enable-https-use-widget": "请启用HTTPS以使用此部件", "no-found-your-camera": "未找到摄像机", "no-permission-camera": "权限被用户拒绝/此站点无权使用摄像机", - "no-timeseries-selected": "未选择 Timeseries", + "no-timeseries-selected": "未选择时间序列值", "secret-key": "密钥", "secret-key-required": "密钥必填", "switch-attribute-value": "切换实体属性值", "switch-camera": "切换摄像机", - "switch-timeseries-value": "切换实体 Timeseries 值", + "switch-timeseries-value": "切换实体时间序列值", "take-photo": "拍照", "time": "时间", - "timeseries-not-allowed": "Timeseries 参数不能用于此部件", + "timeseries-not-allowed": "时间序列参数不能用于此部件", "update-failed": "更新失败", "update-successful": "更新成功", "update-attribute": "更新属性", - "update-timeseries": "更新 Timeseries", + "update-timeseries": "更新时间序列", "value": "数值", "general-settings": "通用设置", "widget-title": "部件标题", @@ -5638,7 +5314,7 @@ "attribute-settings": "属性设置", "widget-mode": "部件模式", "widget-mode-update-attribute": "更新属性", - "widget-mode-update-timeseries": "更新 Timeseries", + "widget-mode-update-timeseries": "更新时间序列", "attribute-scope": "属性范围", "attribute-scope-server": "服务器属性", "attribute-scope-shared": "共享属性", @@ -5669,7 +5345,7 @@ "datakey-type": "数据键类型", "datakey-type-server": "服务器属性(默认)", "datakey-type-shared": "共享属性", - "datakey-type-timeseries": "Timeseries", + "datakey-type-timeseries": "时间序列", "datakey-value-type": "数据键值类型", "datakey-value-type-string": "字符串", "datakey-value-type-double": "双精度", @@ -5827,7 +5503,7 @@ "retrieve-value-method-attribute": "订阅属性获取值", "retrieve-value-method-timeseries": "订阅时间序列获取值", "attribute-value-key": "属性键", - "timeseries-value-key": "Timeseries键", + "timeseries-value-key": "时间序列键", "get-value-method": "RPC获取值方法", "parse-value-function": "解析值的函数", "update-value-settings": "更新值设置", diff --git a/ui-ngx/src/assets/widget/power-button/default-layout.svg b/ui-ngx/src/assets/widget/power-button/default-layout.svg new file mode 100644 index 0000000000..74210bff64 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/default-layout.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg new file mode 100644 index 0000000000..9a502cc8f0 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/default-volume-layout.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/outlined-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-layout.svg new file mode 100644 index 0000000000..9510d68721 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/outlined-layout.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg new file mode 100644 index 0000000000..9e511892b3 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/outlined-volume-layout.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/simplified-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-layout.svg new file mode 100644 index 0000000000..eecb1e9cbe --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/simplified-layout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg new file mode 100644 index 0000000000..500d9a0c97 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/simplified-volume-layout.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/slider/default-layout.svg b/ui-ngx/src/assets/widget/slider/default-layout.svg new file mode 100644 index 0000000000..0b6d1bce33 --- /dev/null +++ b/ui-ngx/src/assets/widget/slider/default-layout.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/slider/extended-layout.svg b/ui-ngx/src/assets/widget/slider/extended-layout.svg new file mode 100644 index 0000000000..923ee8af3d --- /dev/null +++ b/ui-ngx/src/assets/widget/slider/extended-layout.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/slider/simplified-layout.svg b/ui-ngx/src/assets/widget/slider/simplified-layout.svg new file mode 100644 index 0000000000..d2e3936db0 --- /dev/null +++ b/ui-ngx/src/assets/widget/slider/simplified-layout.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 635d330572..ab018a8bc6 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -233,6 +233,36 @@ } } + .tb-flex { + display: flex; + flex: 1; + gap: 8px; + &.row { + flex-direction: row; + } + &.column { + flex-direction: column; + } + &.flex-start { + justify-content: flex-start; + } + &.flex-end { + justify-content: flex-end; + } + &.space-between { + justify-content: space-between; + } + &.align-center { + align-items: center; + } + &.no-gap { + gap: 0; + } + &.fill-width { + width: 100%; + } + } + .tb-form-panel, .tb-form-row { .mat-slide { margin: 0; diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 0750de263f..8ca9fe4e37 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1252,4 +1252,8 @@ mat-icon { .cursor-pointer { cursor: pointer; } + + .no-wrap { + white-space: nowrap; + } } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 2c60a7f009..d78a9a52d6 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2747,7 +2747,14 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== -"@svgdotjs/svg.js@^3.2.0": +"@svgdotjs/svg.filter.js@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.8.tgz#998cb2481a871fa70d7dbaa891c886b335c562d7" + integrity sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog== + dependencies: + "@svgdotjs/svg.js" "^3.1.1" + +"@svgdotjs/svg.js@^3.1.1", "@svgdotjs/svg.js@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz#6baa8cef6778a93818ac18faa2055222e60aa644" integrity sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==