Browse Source

Merge branch 'master' into feature/geofencing-cf

pull/13920/head
dshvaika 1 year ago
parent
commit
4e9b5cf927
  1. 44
      application/pom.xml
  2. 14
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md
  3. 2
      application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg
  4. 2
      application/src/main/data/json/system/scada_symbols/battery-hp.svg
  5. 2
      application/src/main/data/json/system/scada_symbols/conical-tank.svg
  6. 6
      application/src/main/data/json/system/scada_symbols/control-panel-hp.svg
  7. 9
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  8. 2
      application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg
  9. 2
      application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
  10. 4
      application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
  11. 6
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  12. 2
      application/src/main/data/json/system/scada_symbols/elevated-tank.svg
  13. 2
      application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg
  14. 2
      application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg
  15. 2
      application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
  16. 2
      application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg
  17. 2
      application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg
  18. 2
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  19. 2
      application/src/main/data/json/system/scada_symbols/large-conical-tank.svg
  20. 2
      application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
  21. 2
      application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
  22. 2
      application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
  23. 2
      application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
  24. 2
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  25. 2
      application/src/main/data/json/system/scada_symbols/left-heat-pump.svg
  26. 2
      application/src/main/data/json/system/scada_symbols/meter.svg
  27. 2
      application/src/main/data/json/system/scada_symbols/pool.svg
  28. 2
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  29. 2
      application/src/main/data/json/system/scada_symbols/right-heat-pump.svg
  30. 12
      application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg
  31. 12
      application/src/main/data/json/system/scada_symbols/sand-filter.svg
  32. 4
      application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
  33. 6
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  34. 2
      application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg
  35. 2
      application/src/main/data/json/system/scada_symbols/small-left-meter.svg
  36. 2
      application/src/main/data/json/system/scada_symbols/small-meter.svg
  37. 2
      application/src/main/data/json/system/scada_symbols/small-right-center.svg
  38. 2
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  39. 2
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  40. 2
      application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
  41. 2
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  42. 2
      application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
  43. 2
      application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
  44. 2
      application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg
  45. 2
      application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg
  46. 2
      application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg
  47. 2
      application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
  48. 2
      application/src/main/data/json/system/scada_symbols/vertical-tank.svg
  49. 2
      application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg
  50. 2
      application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg
  51. 13
      application/src/main/data/upgrade/basic/schema_update.sql
  52. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  53. 11
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  54. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  55. 20
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  56. 61
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  57. 178
      application/src/main/java/org/thingsboard/server/controller/AiModelController.java
  58. 16
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  59. 60
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  60. 22
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  61. 109
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  62. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  63. 15
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  64. 163
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  65. 20
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java
  66. 40
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  67. 27
      application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java
  68. 86
      application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java
  69. 269
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  70. 5
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  71. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  72. 8
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  73. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  74. 15
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  75. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java
  76. 16
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  77. 150
      application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java
  78. 7
      application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java
  79. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  80. 80
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java
  81. 27
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java
  82. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
  83. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java
  84. 43
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java
  85. 183
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  86. 22
      application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java
  87. 6
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java
  88. 96
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java
  89. 5
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  90. 23
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  91. 100
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  92. 70
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java
  93. 112
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java
  94. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  95. 7
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  96. 17
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  97. 16
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
  98. 12
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  99. 12
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java
  100. 5
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

44
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.2.0-SNAPSHOT</version> <version>4.3.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>application</artifactId> <artifactId>application</artifactId>
@ -186,8 +186,8 @@
<artifactId>jjwt</artifactId> <artifactId>jjwt</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.freemarker</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>freemarker</artifactId> <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
@ -381,6 +381,44 @@
<groupId>org.rocksdb</groupId> <groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId> <artifactId>rocksdbjni</artifactId>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
<artifactId>azure-core-test</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
<build> <build>

14
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md

@ -32,19 +32,7 @@ docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup
After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration.
Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings.
First, please update docker compose file version. Find next snippet: Update volume mounts. Locate the following snippet:
```text
version: '3.0'
...
```
And replace it with:
```text
version: '3.8'
...
```
Then update volume mounts. Locate the following snippet:
```text ```text
volumes: volumes:
- ~/.mytb-edge-data:/data - ~/.mytb-edge-data:/data

2
application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

2
application/src/main/data/json/system/scada_symbols/battery-hp.svg

@ -459,7 +459,7 @@
<circle cx="516" cy="102.5" r="12"/> <circle cx="516" cy="102.5" r="12"/>
<circle cx="516" cy="101.5" r="11" stroke="#000" stroke-opacity=".87" stroke-width="2"/> <circle cx="516" cy="101.5" r="11" stroke="#000" stroke-opacity=".87" stroke-width="2"/>
<circle cx="132" cy="101.5" r="11" stroke="#1a1a1a" stroke-width="2"/> <circle cx="132" cy="101.5" r="11" stroke="#1a1a1a" stroke-width="2"/>
</g><circle cx="204" cy="102" r="10" fill="#198038" tb:tag="indicator"/><text x="221" y="105.37933" fill="#000000" font-family="Roboto" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">ON</tspan></text><path d="m103 200h-6s7.858-27.047 14-44c6.324-17.455 18-44 18-44h6s-11.676 26.545-18 44c-6.142 16.953-14 44-14 44z" fill="#1a1a1a" tb:tag="left-bottom-connector"/><path d="m103 0h-6s-6.4394 26.077-9.5 43c-3.2977 18.234-6.5 47-6.5 47h6s2.7023-26.766 6-45c3.0606-16.923 10-45 10-45z" fill="#1a1a1a" tb:tag="left-top-connector"/><path d="m497 0h6s6.439 26.077 9.5 43c3.298 18.234 6.5 47 6.5 47h-6s-2.702-26.766-6-45c-3.061-16.923-10-45-10-45z" fill="#1a1a1a" tb:tag="right-top-connector"/><path d="m0 100h73" stroke="#1a1a1a" stroke-width="6" tb:tag="left-connector"/><path d="m527 100h73" stroke="#1a1a1a" stroke-width="6" tb:tag="right-connector"/><path d="m497 200h6s-7.858-27.047-14-44c-6.324-17.455-18-44-18-44h-6s11.676 26.545 18 44c6.142 16.953 14 44 14 44z" fill="#1a1a1a" tb:tag="right-bottom-connector"/><path d="m201.8 40s-201.8 0-201.8 20.435v100.15c0 0.80861 5.3727 1.4153 12 1.4153h576c6.627 0 12-0.60671 12-1.4153v-100.15c0-20.435-198.21-20.435-198.21-20.435h-101.79zm201.21 24.766c-3.8661 0-6.9999 0.38235-6.9999 0.854v91.622c0 0.47165 3.1341 0.854 6.9999 0.854h43.998c3.8661 0 6.9999-0.38235 6.9999-0.854v-91.622c0-0.47165-3.1341-0.854-6.9999-0.854z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> </g><circle cx="204" cy="102" r="10" fill="#198038" tb:tag="indicator"/><text x="221" y="105.37933" fill="#000000" font-family="Roboto, sans-serif" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">ON</tspan></text><path d="m103 200h-6s7.858-27.047 14-44c6.324-17.455 18-44 18-44h6s-11.676 26.545-18 44c-6.142 16.953-14 44-14 44z" fill="#1a1a1a" tb:tag="left-bottom-connector"/><path d="m103 0h-6s-6.4394 26.077-9.5 43c-3.2977 18.234-6.5 47-6.5 47h6s2.7023-26.766 6-45c3.0606-16.923 10-45 10-45z" fill="#1a1a1a" tb:tag="left-top-connector"/><path d="m497 0h6s6.439 26.077 9.5 43c3.298 18.234 6.5 47 6.5 47h-6s-2.702-26.766-6-45c-3.061-16.923-10-45-10-45z" fill="#1a1a1a" tb:tag="right-top-connector"/><path d="m0 100h73" stroke="#1a1a1a" stroke-width="6" tb:tag="left-connector"/><path d="m527 100h73" stroke="#1a1a1a" stroke-width="6" tb:tag="right-connector"/><path d="m497 200h6s-7.858-27.047-14-44c-6.324-17.455-18-44-18-44h-6s11.676 26.545 18 44c6.142 16.953 14 44 14 44z" fill="#1a1a1a" tb:tag="right-bottom-connector"/><path d="m201.8 40s-201.8 0-201.8 20.435v100.15c0 0.80861 5.3727 1.4153 12 1.4153h576c6.627 0 12-0.60671 12-1.4153v-100.15c0-20.435-198.21-20.435-198.21-20.435h-101.79zm201.21 24.766c-3.8661 0-6.9999 0.38235-6.9999 0.854v91.622c0 0.47165 3.1341 0.854 6.9999 0.854h43.998c3.8661 0 6.9999-0.38235 6.9999-0.854v-91.622c0-0.47165-3.1341-0.854-6.9999-0.854z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

2
application/src/main/data/json/system/scada_symbols/conical-tank.svg

@ -267,7 +267,7 @@
</g><g transform="translate(0,44)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </g><g transform="translate(0,44)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><ellipse cx="500" cy="16" rx="496" ry="16" fill="#5D5C5C"/><rect transform="rotate(-90 451.5 998.5)" x="451.5" y="998.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><g style="display: none;" tb:tag="stand"> </g><ellipse cx="500" cy="16" rx="496" ry="16" fill="#5D5C5C"/><rect transform="rotate(-90 451.5 998.5)" x="451.5" y="998.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><g style="display: none;" tb:tag="stand">
<rect x="107" y="949" width="80" height="24" rx="7" fill="#fff" style=""/> <rect x="107" y="949" width="80" height="24" rx="7" fill="#fff" style=""/>
<rect x="107" y="949" width="80" height="24" rx="7" fill="url(#paint629_linear_2901_184325)" style=""/> <rect x="107" y="949" width="80" height="24" rx="7" fill="url(#paint629_linear_2901_184325)" style=""/>

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

6
application/src/main/data/json/system/scada_symbols/control-panel-hp.svg

@ -320,13 +320,13 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect width="400" height="200" rx="8" fill="#dedede" tb:tag="background"/><rect x="1" y="1" width="398" height="198" rx="7" stroke="#000" stroke-opacity=".87" stroke-width="2"/><text x="198.7832" y="57.511719" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Heat pump</tspan></text><g tb:tag="onButton"> <rect width="400" height="200" rx="8" fill="#dedede" tb:tag="background"/><rect x="1" y="1" width="398" height="198" rx="7" stroke="#000" stroke-opacity=".87" stroke-width="2"/><text x="198.7832" y="57.511719" fill="#000000" font-family="Roboto, sans-serif" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Heat pump</tspan></text><g tb:tag="onButton">
<rect x="29" y="100" width="156" height="72" rx="9.8143" fill="#999" style="stroke-width:.95761"/> <rect x="29" y="100" width="156" height="72" rx="9.8143" fill="#999" style="stroke-width:.95761"/>
<path d="m29.957 109.03c0-4.4589 3.9482-8.0734 8.8189-8.0734h136.45c4.8707 0 8.8189 3.6144 8.8189 8.0734v53.938c0 4.4589-3.9482 8.0743-8.8189 8.0743h-136.45c-4.8707 0-8.8189-3.6153-8.8189-8.0743z" shape-rendering="crispEdges" stroke="#999" stroke-width="1.9147"/> <path d="m29.957 109.03c0-4.4589 3.9482-8.0734 8.8189-8.0734h136.45c4.8707 0 8.8189 3.6144 8.8189 8.0734v53.938c0 4.4589-3.9482 8.0743-8.8189 8.0743h-136.45c-4.8707 0-8.8189-3.6153-8.8189-8.0743z" shape-rendering="crispEdges" stroke="#999" stroke-width="1.9147"/>
<text x="107.44609" y="138.7375" fill="#000000" font-family="Roboto" font-size="34px" font-weight="500" text-anchor="middle" tb:tag="onLabel" xml:space="preserve"><tspan dominant-baseline="middle">On</tspan></text> <text x="107.44609" y="138.7375" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="500" text-anchor="middle" tb:tag="onLabel" xml:space="preserve"><tspan dominant-baseline="middle">On</tspan></text>
</g><g tb:tag="offButton"> </g><g tb:tag="offButton">
<rect x="214" y="100" width="156" height="72" rx="9.8143" fill="#dedede" style="stroke-width:.95761"/> <rect x="214" y="100" width="156" height="72" rx="9.8143" fill="#dedede" style="stroke-width:.95761"/>
<path d="m214.96 109.03c0-4.4589 3.9482-8.0734 8.8189-8.0734h136.45c4.8707 0 8.8189 3.6144 8.8189 8.0734v53.938c0 4.4589-3.9482 8.0743-8.8189 8.0743h-136.45c-4.8707 0-8.8189-3.6153-8.8189-8.0743z" shape-rendering="crispEdges" stroke="#999" stroke-width="1.9147"/> <path d="m214.96 109.03c0-4.4589 3.9482-8.0734 8.8189-8.0734h136.45c4.8707 0 8.8189 3.6144 8.8189 8.0734v53.938c0 4.4589-3.9482 8.0743-8.8189 8.0743h-136.45c-4.8707 0-8.8189-3.6153-8.8189-8.0743z" shape-rendering="crispEdges" stroke="#999" stroke-width="1.9147"/>
<text x="290.68408" y="139.33516" fill="#000000" font-family="Roboto" font-size="34px" font-weight="500" text-anchor="middle" tb:tag="offLabel" xml:space="preserve"><tspan dominant-baseline="middle">Off</tspan></text> <text x="290.68408" y="139.33516" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="500" text-anchor="middle" tb:tag="offLabel" xml:space="preserve"><tspan dominant-baseline="middle">Off</tspan></text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

9
application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg

@ -489,14 +489,13 @@
"type": "number", "type": "number",
"default": 6, "default": 6,
"required": true, "required": true,
"subLabel": "Main", "divider": false,
"divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1, "step": 1,
"disabled": false "disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
@ -584,4 +583,4 @@
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" id="path12" fill="#1A1A1A" tb:tag="line-color"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" id="path10" fill="#1A1A1A" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke-width="6" id="path8" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke-width="6" id="path6" stroke="#1A1A1A" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke-width="6" id="path4" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke-width="6" id="path2" stroke="#1A1A1A" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="rightLine"/><g tb:tag="bottomLine"/></g> <path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" id="path12" fill="#1A1A1A" tb:tag="line-color"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" id="path10" fill="#1A1A1A" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke-width="6" id="path8" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke-width="6" id="path6" stroke="#1A1A1A" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke-width="6" id="path4" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke-width="6" id="path2" stroke="#1A1A1A" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="rightLine"/><g tb:tag="bottomLine"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

2
application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg

@ -425,7 +425,7 @@
<rect width="200" height="400" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="198" height="398" stroke="#000" stroke-opacity=".87" stroke-width="2"/><g tb:tag="breaker"> <rect width="200" height="400" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="198" height="398" stroke="#000" stroke-opacity=".87" stroke-width="2"/><g tb:tag="breaker">
<path d="m50 100h100v198c0 1.105-0.895 2-2 2h-96c-1.1046 0-2-0.895-2-2v-198z"/> <path d="m50 100h100v198c0 1.105-0.895 2-2 2h-96c-1.1046 0-2-0.895-2-2v-198z"/>
<path d="m51 101h98v197c0 0.552-0.448 1-1 1h-96c-0.5523 0-1-0.448-1-1v-197z" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="label-box"/> <path d="m51 101h98v197c0 0.552-0.448 1-1 1h-96c-0.5523 0-1-0.448-1-1v-197z" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="label-box"/>
<text x="100" y="201.65533" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="start">ON</tspan></text> <text x="100" y="201.65533" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="start">ON</tspan></text>
<g tb:tag="breaker-trigger"> <g tb:tag="breaker-trigger">
<rect x="24" y="100" width="152" height="40" rx="2" fill="#999"/> <rect x="24" y="100" width="152" height="40" rx="2" fill="#999"/>
<rect x="25" y="101" width="150" height="38" rx="1" stroke="#000" stroke-opacity=".87" stroke-width="2"/> <rect x="25" y="101" width="150" height="38" rx="1" stroke="#000" stroke-opacity=".87" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

2
application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg

@ -563,7 +563,7 @@
</g><g transform="translate(-1,409)" filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </g><g transform="translate(-1,409)" filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><ellipse cx="300" cy="16" rx="292" ry="16" fill="#5D5C5C"/><path d="m201.79 0s-201.79 0-201.79 167.5v820.9c0 6.628 5.3726 11.601 12 11.601h576c6.627 0 12-4.973 12-11.601v-820.9c0-167.5-198.21-167.5-198.21-167.5h-101.79zm201.21 203c-3.866 0-7 3.134-7 7v751c0 3.866 3.134 7 7 7h44c3.866 0 7-3.134 7-7v-751c0-3.866-3.134-7-7-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><ellipse cx="300" cy="16" rx="292" ry="16" fill="#5D5C5C"/><path d="m201.79 0s-201.79 0-201.79 167.5v820.9c0 6.628 5.3726 11.601 12 11.601h576c6.627 0 12-4.973 12-11.601v-820.9c0-167.5-198.21-167.5-198.21-167.5h-101.79zm201.21 203c-3.866 0-7 3.134-7 7v751c0 3.866 3.134 7 7 7h44c3.866 0 7-3.134 7-7v-751c0-3.866-3.134-7-7-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_1690_149726" x1="600" x2=".018833" y1="510.96" y2="504.56" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1690_149726" x1="600" x2=".018833" y1="510.96" y2="504.56" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/> <stop stop-color="#020202" stop-opacity=".35" offset="0"/>

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

4
application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg

@ -586,13 +586,13 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<text x="409.16602" y="97.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="401.15625" y="345.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g fill="#666" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" style="display: none;" tb:tag="minMaxValue"> <text x="409.16602" y="97.234375" fill="#000000" font-family="Roboto, sans-serif" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="401.15625" y="345.75" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g fill="#666" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" text-anchor="middle" style="display: none;" tb:tag="minMaxValue">
<text x="88.070312" y="268" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text> <text x="88.070312" y="268" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text>
<text x="705.02344" y="268" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text> <text x="705.02344" y="268" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text>
</g><g transform="translate(180)" tb:tag="valueArrowPosition"> </g><g transform="translate(180)" tb:tag="valueArrowPosition">
<path d="m80 179 24-42h-48z" fill="#666" tb:tag="valuePointer"/> <path d="m80 179 24-42h-48z" fill="#666" tb:tag="valuePointer"/>
</g><g transform="translate(180)" tb:tag="valueTextPositon"> </g><g transform="translate(180)" tb:tag="valueTextPositon">
<text x="79.779297" y="273.125" fill="#002878" font-family="Roboto" font-size="60px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">26</tspan></text> <text x="79.779297" y="273.125" fill="#002878" font-family="Roboto, sans-serif" font-size="60px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">26</tspan></text>
</g><g stroke="#000" tb:tag="scale"> </g><g stroke="#000" tb:tag="scale">
<rect x="80.5" y="183.5" width="652" height="41" fill="#c8dff7" tb:tag="scaleBackground"/> <rect x="80.5" y="183.5" width="652" height="41" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect x="80.5" y="183.5" width="164" height="41" fill="#ebebeb" tb:tag="lowWarningScale"/> <rect x="80.5" y="183.5" width="164" height="41" fill="#ebebeb" tb:tag="lowWarningScale"/>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

6
application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg

@ -579,19 +579,19 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale"> <text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto, sans-serif" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/> <rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/> <rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/> <rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="81" fill="#666" tb:tag="lowCriticalScale"/> <rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="81" fill="#666" tb:tag="lowCriticalScale"/>
<rect x="176.5" y="83.5" width="41" height="81" fill="#666" tb:tag="highCriticalScale"/> <rect x="176.5" y="83.5" width="41" height="81" fill="#666" tb:tag="highCriticalScale"/>
</g><g fill="#666" font-family="Roboto" font-size="32px" font-weight="500" style="display: none;" tb:tag="minMaxValue"> </g><g fill="#666" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" style="display: none;" tb:tag="minMaxValue">
<text x="237.48438" y="97" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text> <text x="237.48438" y="97" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text>
<text x="238.89062" y="727" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text> <text x="238.89062" y="727" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text>
</g><g transform="translate(0,-180)" tb:tag="valueArrowPosition"> </g><g transform="translate(0,-180)" tb:tag="valueArrowPosition">
<path d="m168 735-42-24v48z" fill="#666" tb:tag="valuePointer"/> <path d="m168 735-42-24v48z" fill="#666" tb:tag="valuePointer"/>
</g><g transform="translate(0,-180)" tb:tag="valueTextPositon"> </g><g transform="translate(0,-180)" tb:tag="valueTextPositon">
<text x="223.60547" y="739.125" fill="#002878" font-family="Roboto" font-size="60px" font-weight="400" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="middle">26</tspan></text> <text x="223.60547" y="739.125" fill="#002878" font-family="Roboto, sans-serif" font-size="60px" font-weight="400" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="middle">26</tspan></text>
</g><g transform="translate(0,-400)" style="display: none;" tb:tag="target"> </g><g transform="translate(0,-400)" style="display: none;" tb:tag="target">
<rect transform="rotate(-45)" x="-379.76" y="636.31" width="22" height="22" fill="#dedede" stroke="#000" stroke-width="2" tb:tag="targetBackground" style=""/> <rect transform="rotate(-45)" x="-379.76" y="636.31" width="22" height="22" fill="#dedede" stroke="#000" stroke-width="2" tb:tag="targetBackground" style=""/>
</g><path d="m134.53 0s-134.53 0-134.53 134v656.72c0 5.3024 3.5817 9.2808 8 9.2808h384c4.418 0 8-3.9784 8-9.2808v-656.72c0-134-132.14-134-132.14-134h-67.86zm134.14 162.4c-2.5773 0-4.6667 2.5072-4.6667 5.6v600.8c0 3.0928 2.0893 5.6 4.6667 5.6h29.333c2.5773 0 4.6667-2.5072 4.6667-5.6v-600.8c0-3.0928-2.0893-5.6-4.6667-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/> </g><path d="m134.53 0s-134.53 0-134.53 134v656.72c0 5.3024 3.5817 9.2808 8 9.2808h384c4.418 0 8-3.9784 8-9.2808v-656.72c0-134-132.14-134-132.14-134h-67.86zm134.14 162.4c-2.5773 0-4.6667 2.5072-4.6667 5.6v600.8c0 3.0928 2.0893 5.6 4.6667 5.6h29.333c2.5773 0 4.6667-2.5072 4.6667-5.6v-600.8c0-3.0928-2.0893-5.6-4.6667-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/>

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

2
application/src/main/data/json/system/scada_symbols/elevated-tank.svg

@ -557,7 +557,7 @@
</g><path d="m3.0002 1143.6-1.7e-4 -141.73c16.904 3.91 46.87 7.48 86.628 10.71 43.009 3.48 97.728 6.56 160.31 9.17 125.17 5.22 281.88 8.54 439.46 9.36 157.58 0.81 316.06-0.88 444.77-5.69 64.35-2.41 121.29-5.59 166.96-9.64 42.72-3.78 75.8-8.33 95.87-13.77v141.48c0 4.44-2.78 7.94-6.85 8.68-141.99 25.81-431.84 36.08-714.04 34.75-282.22-1.34-556.2-14.27-666.45-34.72-3.9871-0.74-6.6571-4.16-6.6571-8.6z" stroke="#647484" stroke-width="6"/><path d="m1023 1030v145" stroke="#647484" stroke-width="6"/><path d="m701 1030v150" stroke="#647484" stroke-width="6"/><path d="m379 1023v151" stroke="#647484" stroke-width="6"/><path d="m0 1147c125.45 33.35 1246.3 46.29 1400 0v61.71c0 5.74-3.68 10.62-9.33 11.64-284.6 51.35-1160.4 40.74-1381.5 0.03-5.601-1.04-9.1261-5.87-9.1261-11.56l-1.5804e-4 -61.82z" fill="#E5E5E5" tb:tag="background"/><path d="m0 1147c125.45 33.35 1246.3 46.29 1400 0v61.71c0 5.74-3.68 10.62-9.33 11.64-284.6 51.35-1160.4 40.74-1381.5 0.03-5.601-1.04-9.1261-5.87-9.1261-11.56l-1.5804e-4 -61.82z" fill="url(#paint301_linear_2188_188555)"/><path d="m1.5002 1208.8-1.6e-4 -59.89c16.364 4.02 46.959 7.72 88.25 11.04 42.982 3.45 97.679 6.52 160.25 9.11 125.15 5.18 281.84 8.47 439.41 9.28s316.02-0.87 444.71-5.65c64.33-2.38 121.24-5.55 166.88-9.56 44.11-3.88 77.81-8.56 97.5-14.15v59.71c0 5.09-3.23 9.28-8.1 10.16-142.14 25.65-432.11 35.84-714.3 34.52s-556.29-14.16-666.71-34.49c-4.797-0.88-7.8977-5.01-7.8977-10.08z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m700 0 597.89 200.16c4 1.341 3.46 7.138-0.73 7.694-189.06 25.141-364.28 25.141-597.16 25.141-230.8 0-478.09 0-600.31-24.472-3.9498-0.791-4.1701-6.156-0.3503-7.435l600.66-201.09z" fill="#E5E5E5" tb:tag="background"/><path d="m700 0 597.89 200.16c4 1.341 3.46 7.138-0.73 7.694-189.06 25.141-364.28 25.141-597.16 25.141-230.8 0-478.09 0-600.31-24.472-3.9498-0.791-4.1701-6.156-0.3503-7.435l600.66-201.09z" fill="url(#paint302_linear_2188_188555)"/><path d="m99.815 202.52 600.18-200.93 597.41 200.01c2.51 0.84 2.15 4.439-0.44 4.785-188.96 25.125-364.07 25.127-596.97 25.127-115.41 0-234.9 0-340.83-3.058-105.95-3.059-198.21-9.177-259.19-21.385-2.3046-0.462-2.6651-3.706-0.1686-4.541z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g filter="url(#filter0_ii_2188_188555)" tb:tag="value-box"> </g><path d="m3.0002 1143.6-1.7e-4 -141.73c16.904 3.91 46.87 7.48 86.628 10.71 43.009 3.48 97.728 6.56 160.31 9.17 125.17 5.22 281.88 8.54 439.46 9.36 157.58 0.81 316.06-0.88 444.77-5.69 64.35-2.41 121.29-5.59 166.96-9.64 42.72-3.78 75.8-8.33 95.87-13.77v141.48c0 4.44-2.78 7.94-6.85 8.68-141.99 25.81-431.84 36.08-714.04 34.75-282.22-1.34-556.2-14.27-666.45-34.72-3.9871-0.74-6.6571-4.16-6.6571-8.6z" stroke="#647484" stroke-width="6"/><path d="m1023 1030v145" stroke="#647484" stroke-width="6"/><path d="m701 1030v150" stroke="#647484" stroke-width="6"/><path d="m379 1023v151" stroke="#647484" stroke-width="6"/><path d="m0 1147c125.45 33.35 1246.3 46.29 1400 0v61.71c0 5.74-3.68 10.62-9.33 11.64-284.6 51.35-1160.4 40.74-1381.5 0.03-5.601-1.04-9.1261-5.87-9.1261-11.56l-1.5804e-4 -61.82z" fill="#E5E5E5" tb:tag="background"/><path d="m0 1147c125.45 33.35 1246.3 46.29 1400 0v61.71c0 5.74-3.68 10.62-9.33 11.64-284.6 51.35-1160.4 40.74-1381.5 0.03-5.601-1.04-9.1261-5.87-9.1261-11.56l-1.5804e-4 -61.82z" fill="url(#paint301_linear_2188_188555)"/><path d="m1.5002 1208.8-1.6e-4 -59.89c16.364 4.02 46.959 7.72 88.25 11.04 42.982 3.45 97.679 6.52 160.25 9.11 125.15 5.18 281.84 8.47 439.41 9.28s316.02-0.87 444.71-5.65c64.33-2.38 121.24-5.55 166.88-9.56 44.11-3.88 77.81-8.56 97.5-14.15v59.71c0 5.09-3.23 9.28-8.1 10.16-142.14 25.65-432.11 35.84-714.3 34.52s-556.29-14.16-666.71-34.49c-4.797-0.88-7.8977-5.01-7.8977-10.08z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m700 0 597.89 200.16c4 1.341 3.46 7.138-0.73 7.694-189.06 25.141-364.28 25.141-597.16 25.141-230.8 0-478.09 0-600.31-24.472-3.9498-0.791-4.1701-6.156-0.3503-7.435l600.66-201.09z" fill="#E5E5E5" tb:tag="background"/><path d="m700 0 597.89 200.16c4 1.341 3.46 7.138-0.73 7.694-189.06 25.141-364.28 25.141-597.16 25.141-230.8 0-478.09 0-600.31-24.472-3.9498-0.791-4.1701-6.156-0.3503-7.435l600.66-201.09z" fill="url(#paint302_linear_2188_188555)"/><path d="m99.815 202.52 600.18-200.93 597.41 200.01c2.51 0.84 2.15 4.439-0.44 4.785-188.96 25.125-364.07 25.127-596.97 25.127-115.41 0-234.9 0-340.83-3.058-105.95-3.059-198.21-9.177-259.19-21.385-2.3046-0.462-2.6651-3.706-0.1686-4.541z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g filter="url(#filter0_ii_2188_188555)" tb:tag="value-box">
<path d="m580 634c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#fff" tb:tag="value-box-background"/> <path d="m580 634c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#fff" tb:tag="value-box-background"/>
<path d="m581.5 634c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m581.5 634c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="705.52533" y="666.35553" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="705.52533" y="666.35553" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><g fill="#D9D9D9" filter="url(#filter1_d_2188_188555)" stroke="#727171" stroke-width="3"> </g><g fill="#D9D9D9" filter="url(#filter1_d_2188_188555)" stroke="#727171" stroke-width="3">
<rect transform="matrix(0 -1 .99255 -.12187 1087.2 414.35)" x="1.4888" y="-1.6828" width="11" height="77" rx="5.5"/> <rect transform="matrix(0 -1 .99255 -.12187 1087.2 414.35)" x="1.4888" y="-1.6828" width="11" height="77" rx="5.5"/>
<rect transform="matrix(0 -1 .99255 -.12187 1087.2 368.35)" x="1.4888" y="-1.6828" width="11" height="77" rx="5.5"/> <rect transform="matrix(0 -1 .99255 -.12187 1087.2 368.35)" x="1.4888" y="-1.6828" width="11" height="77" rx="5.5"/>

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

2
application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg

@ -474,7 +474,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="1" width="398" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="143" width="302" height="114" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box"/><text x="199.83594" y="217.64844" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="start">000023</tspan></text><text x="199.89453" y="341.65625" fill="#000000" font-family="Roboto" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><text x="199.70117" y="83.648438" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><path d="m134.53-2e-4s-134.53 0-134.53 67v328.36c0 2.6512 3.5818 4.6404 8 4.6404h384c4.418 0 8-1.9892 8-4.6404v-328.36c0-67-132.14-67-132.14-67h-67.858zm134.14 81.2c-2.5774 0-4.6666 1.2536-4.6666 2.8v300.4c0 1.5464 2.0894 2.8 4.6666 2.8h29.332c2.5774 0 4.6666-1.2536 4.6666-2.8v-300.4c0-1.5464-2.0894-2.8-4.6666-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="1" y="1" width="398" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="143" width="302" height="114" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box"/><text x="199.83594" y="217.64844" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="start">000023</tspan></text><text x="199.89453" y="341.65625" fill="#000000" font-family="Roboto, sans-serif" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><text x="199.70117" y="83.648438" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><path d="m134.53-2e-4s-134.53 0-134.53 67v328.36c0 2.6512 3.5818 4.6404 8 4.6404h384c4.418 0 8-1.9892 8-4.6404v-328.36c0-67-132.14-67-132.14-67h-67.858zm134.14 81.2c-2.5774 0-4.6666 1.2536-4.6666 2.8v300.4c0 1.5464 2.0894 2.8 4.6666 2.8h29.332c2.5774 0 4.6666-1.2536 4.6666-2.8v-300.4c0-1.5464-2.0894-2.8-4.6666-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

2
application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg

@ -877,7 +877,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="1" width="598" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="81" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-off-peak"/><rect x="313" y="81" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-night"/><rect x="49" y="237" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-peak"/><rect x="313" y="237" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-export"/><text x="171.2998" y="58.286133" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="off-peak-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="433.2998" y="58.734375" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="169.2998" y="214.23438" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="peak-label" xml:space="default"><tspan dominant-baseline="start">T3</tspan></text><text x="432.31152" y="213.78613" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="export-label" xml:space="default"><tspan dominant-baseline="start">Export</tspan></text><text x="169.45312" y="139.625" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="off-peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="433.45312" y="139.625" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="169.45312" y="295.625" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="433.45312" y="295.625" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="export-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="299.89453" y="371.67578" fill="#000000" font-family="Roboto" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m201.8-2e-4s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6404 12 4.6404h576c6.627 0 12-1.9892 12-4.6404v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" fill="#000" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="1" y="1" width="598" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="81" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-off-peak"/><rect x="313" y="81" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-night"/><rect x="49" y="237" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-peak"/><rect x="313" y="237" width="238" height="82" rx="3" fill="#dedede" stroke="#1a1a1a" stroke-width="2" tb:tag="value-box-export"/><text x="171.2998" y="58.286133" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="off-peak-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="433.2998" y="58.734375" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="169.2998" y="214.23438" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="peak-label" xml:space="default"><tspan dominant-baseline="start">T3</tspan></text><text x="432.31152" y="213.78613" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="export-label" xml:space="default"><tspan dominant-baseline="start">Export</tspan></text><text x="169.45312" y="139.625" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="off-peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="433.45312" y="139.625" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="169.45312" y="295.625" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="433.45312" y="295.625" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="export-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="299.89453" y="371.67578" fill="#000000" font-family="Roboto, sans-serif" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m201.8-2e-4s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6404 12 4.6404h576c6.627 0 12-1.9892 12-4.6404v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" fill="#000" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

2
application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg

@ -489,7 +489,7 @@
<rect x="1" y="1" width="798" height="542" rx="31" stroke="#000" stroke-opacity=".87" stroke-width="2"/> <rect x="1" y="1" width="798" height="542" rx="31" stroke="#000" stroke-opacity=".87" stroke-width="2"/>
<rect x="80" y="72.001" width="180" height="128" rx="8"/> <rect x="80" y="72.001" width="180" height="128" rx="8"/>
<rect x="81" y="73.001" width="178" height="126" rx="7" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="value-box-background"/> <rect x="81" y="73.001" width="178" height="126" rx="7" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="value-box-background"/>
<text x="170.51953" y="139.75" dominant-baseline="middle" fill="#002878" font-family="Roboto" font-size="40px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>27</tspan></text> <text x="170.51953" y="139.75" dominant-baseline="middle" fill="#002878" font-family="Roboto, sans-serif" font-size="40px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>27</tspan></text>
<ellipse cx="542" cy="284" rx="180" ry="180" fill="#fff"/> <ellipse cx="542" cy="284" rx="180" ry="180" fill="#fff"/>
<path d="m720.5 284c0 98.583-79.918 178.5-178.5 178.5s-178.5-79.918-178.5-178.5 79.918-178.5 178.5-178.5 178.5 79.918 178.5 178.5z" stroke="#000" stroke-opacity=".12" stroke-width="2.9985"/> <path d="m720.5 284c0 98.583-79.918 178.5-178.5 178.5s-178.5-79.918-178.5-178.5 79.918-178.5 178.5-178.5 178.5 79.918 178.5 178.5z" stroke="#000" stroke-opacity=".12" stroke-width="2.9985"/>
<g clip-path="url(#clip1_3215_7169)"> <g clip-path="url(#clip1_3215_7169)">

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

2
application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg

@ -423,7 +423,7 @@
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="1" width="398" height="198" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><circle cx="49" cy="100" r="19" stroke="#1a1a1a" stroke-width="2"/><circle cx="351" cy="100" r="19" stroke="#1a1a1a" stroke-width="2"/><path d="m0 100h31" stroke="#1A1A1A" stroke-width="6"/><path d="m369 100h31" stroke="#1A1A1A" stroke-width="6"/><g tb:tag="breaker"> <rect x="1" y="1" width="398" height="198" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><circle cx="49" cy="100" r="19" stroke="#1a1a1a" stroke-width="2"/><circle cx="351" cy="100" r="19" stroke="#1a1a1a" stroke-width="2"/><path d="m0 100h31" stroke="#1A1A1A" stroke-width="6"/><path d="m369 100h31" stroke="#1A1A1A" stroke-width="6"/><g tb:tag="breaker">
<path d="m101 51h198v97c0 0.552-0.448 1-1 1h-196c-0.552 0-1-0.448-1-1v-97z" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="label-box"/> <path d="m101 51h198v97c0 0.552-0.448 1-1 1h-196c-0.552 0-1-0.448-1-1v-97z" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="label-box"/>
<text x="200.83984" y="114.56055" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="start">ON</tspan></text> <text x="200.83984" y="114.56055" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="start">ON</tspan></text>
<rect x="261" y="25" width="38" height="150" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2" tb:tag="breaker-trigger"/> <rect x="261" y="25" width="38" height="150" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2" tb:tag="breaker-trigger"/>
</g><g fill="#d12730" style="display: none;" tb:tag="critical"> </g><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

2
application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg

@ -364,7 +364,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="17" width="598" height="366" rx="5" fill="#fff" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="background"/><rect x="26" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="26" y="383" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="426" y="383" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="226" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="426" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="66" cy="88" r="10" fill="#198038" tb:tag="indicator"/><text x="83.31543" y="91.589844" fill="#000000" font-family="Roboto" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Connected</tspan></text><path d="m201.8 0s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6403 12 4.6403h576c6.627 0 12-1.9892 12-4.6403v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" tb:tag="clickArea"/><g transform="translate(0,316)" fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="1" y="17" width="598" height="366" rx="5" fill="#fff" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="background"/><rect x="26" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="26" y="383" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="426" y="383" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="226" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="426" y="1" width="148" height="16" rx="1" fill="#999" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="66" cy="88" r="10" fill="#198038" tb:tag="indicator"/><text x="83.31543" y="91.589844" fill="#000000" font-family="Roboto, sans-serif" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Connected</tspan></text><path d="m201.8 0s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6403 12 4.6403h576c6.627 0 12-1.9892 12-4.6403v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" tb:tag="clickArea"/><g transform="translate(0,316)" fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

2
application/src/main/data/json/system/scada_symbols/horizontal-tank.svg

@ -572,7 +572,7 @@
</mask><g filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </mask><g filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><g transform="matrix(1 0 0 .5 .64 .00025)"> </g><g transform="matrix(1 0 0 .5 .64 .00025)">
<path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/> <path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/>
</g><defs> </g><defs>

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

2
application/src/main/data/json/system/scada_symbols/large-conical-tank.svg

@ -268,7 +268,7 @@
</g><g transform="translate(0,235)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </g><g transform="translate(0,235)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><ellipse cx="500" cy="16" rx="496" ry="16" fill="#5D5C5C"/><rect transform="rotate(-90 451.5 1398.5)" x="451.5" y="1398.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><g style="display: none;" tb:tag="stand"> </g><ellipse cx="500" cy="16" rx="496" ry="16" fill="#5D5C5C"/><rect transform="rotate(-90 451.5 1398.5)" x="451.5" y="1398.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><g style="display: none;" tb:tag="stand">
<rect x="107" y="1363" width="80" height="24" rx="7" fill="#fff" style=""/> <rect x="107" y="1363" width="80" height="24" rx="7" fill="#fff" style=""/>
<rect x="107" y="1363" width="80" height="24" rx="7" fill="url(#paint629_linear_2901_184330)" style=""/> <rect x="107" y="1363" width="80" height="24" rx="7" fill="url(#paint629_linear_2901_184330)" style=""/>

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

2
application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg

@ -563,7 +563,7 @@
</g><ellipse cx="504" cy="16" rx="496" ry="16" fill="#5D5C5C"/><g filter="url(#filter0_ii_2005_230043)" tb:tag="value-box"> </g><ellipse cx="504" cy="16" rx="496" ry="16" fill="#5D5C5C"/><g filter="url(#filter0_ii_2005_230043)" tb:tag="value-box">
<path d="m380 487c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m380 487c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m381.5 487c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m381.5 487c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="503.10281" y="518.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="503.10281" y="518.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m336.32-5e-4s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m336.32-5e-4s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<filter id="filter0_ii_2005_230043" x="376" y="471" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_2005_230043" x="376" y="471" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

2
application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg

@ -564,7 +564,7 @@
</g><ellipse cx="504" cy="16" rx="496" ry="16" fill="#5D5C5C"/><g filter="url(#filter0_ii_2005_230043)" tb:tag="value-box"> </g><ellipse cx="504" cy="16" rx="496" ry="16" fill="#5D5C5C"/><g filter="url(#filter0_ii_2005_230043)" tb:tag="value-box">
<path d="m380 487c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m380 487c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m381.5 487c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m381.5 487c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="503.10281" y="518.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="503.10281" y="518.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m336.32-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="url(#paint245_linear_1702_237993)" style="fill:url(#paint245_linear_1702_237993)"/><rect x="123.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="url(#paint246_linear_1702_237993)" style="fill:url(#paint246_linear_1702_237993)"/><rect x="799.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 168.69 996.46)" x="168.69" y="996.46" width="676" height="8" fill="#727171"/><rect transform="rotate(-10 158.11 1114.7)" x="158.11" y="1114.7" width="687.16" height="8" fill="#727171"/><path d="m146 1163v-169l32 1v168z" fill="#fff"/><path d="m146 1163v-169l32 1v168z" fill="url(#paint247_linear_1702_237993)" style="fill:url(#paint247_linear_1702_237993)"/><path d="m147.5 1161.5v-165.95l29 0.906v165.05z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m822 1163v-167l32-1v168z" fill="#fff"/><path d="m822 1163v-167l32-1v168z" fill="url(#paint248_linear_1702_237993)" style="fill:url(#paint248_linear_1702_237993)"/><path d="m823.5 1161.5v-164.05l29-0.906v164.95z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 135.43 1006.5)" x="135.43" y="1006.5" width="751.02" height="12" fill="#838282"/><rect transform="rotate(-10 130.48 1136)" x="130.48" y="1136" width="748.07" height="12" fill="#838282"/><circle cx="870" cy="885" r="32" fill="url(#paint249_radial_1702_237993)" style="fill:url(#paint249_radial_1702_237993)"/><circle cx="870" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="130" cy="885" r="32" fill="url(#paint250_radial_1702_237993)" style="fill:url(#paint250_radial_1702_237993)"/><circle cx="130" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint251_linear_1702_237993)" style="fill:url(#paint251_linear_1702_237993)"/><path d="m115.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1702_237993)" style="fill:url(#paint252_linear_1702_237993)"/><path d="m855.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="url(#paint253_linear_1702_237993)" style="fill:url(#paint253_linear_1702_237993)"/><rect x="81.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="url(#paint254_linear_1702_237993)" style="fill:url(#paint254_linear_1702_237993)"/><rect x="821.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs> </g><path d="m336.32-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="url(#paint245_linear_1702_237993)" style="fill:url(#paint245_linear_1702_237993)"/><rect x="123.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="url(#paint246_linear_1702_237993)" style="fill:url(#paint246_linear_1702_237993)"/><rect x="799.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 168.69 996.46)" x="168.69" y="996.46" width="676" height="8" fill="#727171"/><rect transform="rotate(-10 158.11 1114.7)" x="158.11" y="1114.7" width="687.16" height="8" fill="#727171"/><path d="m146 1163v-169l32 1v168z" fill="#fff"/><path d="m146 1163v-169l32 1v168z" fill="url(#paint247_linear_1702_237993)" style="fill:url(#paint247_linear_1702_237993)"/><path d="m147.5 1161.5v-165.95l29 0.906v165.05z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m822 1163v-167l32-1v168z" fill="#fff"/><path d="m822 1163v-167l32-1v168z" fill="url(#paint248_linear_1702_237993)" style="fill:url(#paint248_linear_1702_237993)"/><path d="m823.5 1161.5v-164.05l29-0.906v164.95z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 135.43 1006.5)" x="135.43" y="1006.5" width="751.02" height="12" fill="#838282"/><rect transform="rotate(-10 130.48 1136)" x="130.48" y="1136" width="748.07" height="12" fill="#838282"/><circle cx="870" cy="885" r="32" fill="url(#paint249_radial_1702_237993)" style="fill:url(#paint249_radial_1702_237993)"/><circle cx="870" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="130" cy="885" r="32" fill="url(#paint250_radial_1702_237993)" style="fill:url(#paint250_radial_1702_237993)"/><circle cx="130" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint251_linear_1702_237993)" style="fill:url(#paint251_linear_1702_237993)"/><path d="m115.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1702_237993)" style="fill:url(#paint252_linear_1702_237993)"/><path d="m855.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="url(#paint253_linear_1702_237993)" style="fill:url(#paint253_linear_1702_237993)"/><rect x="81.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="url(#paint254_linear_1702_237993)" style="fill:url(#paint254_linear_1702_237993)"/><rect x="821.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs>
<filter id="filter0_ii_2005_230043" x="376" y="471" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_2005_230043" x="376" y="471" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

2
application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg

@ -564,7 +564,7 @@
</g><g filter="url(#filter0_ii_1711_311697)" tb:tag="value-box"> </g><g filter="url(#filter0_ii_1711_311697)" tb:tag="value-box">
<path d="m380 60c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m380 60c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m381.5 60c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m381.5 60c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="505" y="90" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="505" y="90" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><mask id="path-316-inside-2_1711_311697" fill="white"> </g><mask id="path-316-inside-2_1711_311697" fill="white">
<path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z"/> <path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z"/>
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z" fill="#D9D9D9"/><path d="m7 170h986v-6h-986v6zm986 8h-986v6h986v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-990 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-316-inside-2_1711_311697)"/><path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="url(#paint245_linear_1711_311696)" style="fill:url(#paint245_linear_1711_311696)"/><rect x="123.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="url(#paint246_linear_1711_311696)" style="fill:url(#paint246_linear_1711_311696)"/><rect x="799.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 168.69 996.46)" x="168.69" y="996.46" width="676" height="8" fill="#727171"/><rect transform="rotate(-10 158.11 1114.7)" x="158.11" y="1114.7" width="687.16" height="8" fill="#727171"/><path d="m146 1163v-169l32 1v168z" fill="#fff"/><path d="m146 1163v-169l32 1v168z" fill="url(#paint247_linear_1711_311696)" style="fill:url(#paint247_linear_1711_311696)"/><path d="m147.5 1161.5v-165.95l29 0.906v165.05z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m822 1163v-167l32-1v168z" fill="#fff"/><path d="m822 1163v-167l32-1v168z" fill="url(#paint248_linear_1711_311696)" style="fill:url(#paint248_linear_1711_311696)"/><path d="m823.5 1161.5v-164.05l29-0.906v164.95z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 135.43 1006.5)" x="135.43" y="1006.5" width="751.02" height="12" fill="#838282"/><rect transform="rotate(-10 130.48 1136)" x="130.48" y="1136" width="748.07" height="12" fill="#838282"/><circle cx="870" cy="885" r="32" fill="url(#paint249_radial_1711_311696)" style="fill:url(#paint249_radial_1711_311696)"/><circle cx="870" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="130" cy="885" r="32" fill="url(#paint250_radial_1711_311696)" style="fill:url(#paint250_radial_1711_311696)"/><circle cx="130" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint251_linear_1711_311696)" style="fill:url(#paint251_linear_1711_311696)"/><path d="m115.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1711_311696)" style="fill:url(#paint252_linear_1711_311696)"/><path d="m855.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="url(#paint253_linear_1711_311696)" style="fill:url(#paint253_linear_1711_311696)"/><rect x="81.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="url(#paint254_linear_1711_311696)" style="fill:url(#paint254_linear_1711_311696)"/><rect x="821.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z" fill="#D9D9D9"/><path d="m7 170h986v-6h-986v6zm986 8h-986v6h986v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-990 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-316-inside-2_1711_311697)"/><path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="122" y="1163" width="80" height="24" rx="7" fill="url(#paint245_linear_1711_311696)" style="fill:url(#paint245_linear_1711_311696)"/><rect x="123.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="798" y="1163" width="80" height="24" rx="7" fill="url(#paint246_linear_1711_311696)" style="fill:url(#paint246_linear_1711_311696)"/><rect x="799.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 168.69 996.46)" x="168.69" y="996.46" width="676" height="8" fill="#727171"/><rect transform="rotate(-10 158.11 1114.7)" x="158.11" y="1114.7" width="687.16" height="8" fill="#727171"/><path d="m146 1163v-169l32 1v168z" fill="#fff"/><path d="m146 1163v-169l32 1v168z" fill="url(#paint247_linear_1711_311696)" style="fill:url(#paint247_linear_1711_311696)"/><path d="m147.5 1161.5v-165.95l29 0.906v165.05z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m822 1163v-167l32-1v168z" fill="#fff"/><path d="m822 1163v-167l32-1v168z" fill="url(#paint248_linear_1711_311696)" style="fill:url(#paint248_linear_1711_311696)"/><path d="m823.5 1161.5v-164.05l29-0.906v164.95z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 135.43 1006.5)" x="135.43" y="1006.5" width="751.02" height="12" fill="#838282"/><rect transform="rotate(-10 130.48 1136)" x="130.48" y="1136" width="748.07" height="12" fill="#838282"/><circle cx="870" cy="885" r="32" fill="url(#paint249_radial_1711_311696)" style="fill:url(#paint249_radial_1711_311696)"/><circle cx="870" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="130" cy="885" r="32" fill="url(#paint250_radial_1711_311696)" style="fill:url(#paint250_radial_1711_311696)"/><circle cx="130" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m114 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint251_linear_1711_311696)" style="fill:url(#paint251_linear_1711_311696)"/><path d="m115.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m854 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1711_311696)" style="fill:url(#paint252_linear_1711_311696)"/><path d="m855.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="80" y="1171" width="100" height="29" rx="7" fill="url(#paint253_linear_1711_311696)" style="fill:url(#paint253_linear_1711_311696)"/><rect x="81.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="820" y="1171" width="100" height="29" rx="7" fill="url(#paint254_linear_1711_311696)" style="fill:url(#paint254_linear_1711_311696)"/><rect x="821.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs>

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

2
application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg

@ -563,7 +563,7 @@
</g><g filter="url(#filter0_ii_1711_311697)" tb:tag="value-box"> </g><g filter="url(#filter0_ii_1711_311697)" tb:tag="value-box">
<path d="m380 60c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m380 60c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m381.5 60c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m381.5 60c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="505" y="90" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="505" y="90" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><mask id="path-316-inside-2_1711_311697" fill="white"> </g><mask id="path-316-inside-2_1711_311697" fill="white">
<path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z"/> <path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z"/>
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z" fill="#D9D9D9"/><path d="m7 170h986v-6h-986v6zm986 8h-986v6h986v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-990 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-316-inside-2_1711_311697)"/><path d="m335.68-5e-4s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h986c3.866 0 7 3.134 7 7s-3.134 7-7 7h-986z" fill="#D9D9D9"/><path d="m7 170h986v-6h-986v6zm986 8h-986v6h986v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-990 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-316-inside-2_1711_311697)"/><path d="m335.68-5e-4s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

2
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/left-heat-pump.svg

@ -584,7 +584,7 @@
</g><g filter="url(#filter2_ii_1826_356092)"> </g><g filter="url(#filter2_ii_1826_356092)">
<path d="m124 82c0-6.6274 5.373-12 12-12h157c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-157c-6.627 0-12-5.373-12-12z" fill="#fff" tb:tag="value-box-background"/> <path d="m124 82c0-6.6274 5.373-12 12-12h157c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-157c-6.627 0-12-5.373-12-12z" fill="#fff" tb:tag="value-box-background"/>
<path d="m125 82c0-6.0751 4.925-11 11-11h157c6.075 0 11 4.9249 11 11v56c0 6.075-4.925 11-11 11h-157c-6.075 0-11-4.925-11-11z" stroke="#fff" stroke-width="2"/> <path d="m125 82c0-6.0751 4.925-11 11-11h157c6.075 0 11 4.9249 11 11v56c0 6.075-4.925 11-11 11h-157c-6.075 0-11-4.925-11-11z" stroke="#fff" stroke-width="2"/>
<text x="214.69531" y="113.5625" fill="#000000" font-family="Roboto" font-size="40px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">27</tspan></text> <text x="214.69531" y="113.5625" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">27</tspan></text>
</g><circle cx="530" cy="280.2" r="180" fill="#b6b4b4"/><circle cx="530" cy="280.2" r="180" fill="url(#paint34_radial_1826_356092)" style="fill:url(#paint34_radial_1826_356092)"/><circle cx="530" cy="280.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(349.75 99.867)" tb:tag="fan"> </g><circle cx="530" cy="280.2" r="180" fill="#b6b4b4"/><circle cx="530" cy="280.2" r="180" fill="url(#paint34_radial_1826_356092)" style="fill:url(#paint34_radial_1826_356092)"/><circle cx="530" cy="280.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(349.75 99.867)" tb:tag="fan">
<circle cx="180" cy="180.2" r="180" fill="#b6b4b4"/> <circle cx="180" cy="180.2" r="180" fill="#b6b4b4"/>
<circle cx="180" cy="180.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <circle cx="180" cy="180.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

2
application/src/main/data/json/system/scada_symbols/meter.svg

@ -720,7 +720,7 @@
</g><g filter="url(#filter0_ii_2475_365165)" tb:tag="value-box"> </g><g filter="url(#filter0_ii_2475_365165)" tb:tag="value-box">
<rect x="21.5" y="362" width="56" height="30" rx="4" fill="#f3f3f3" fill-opacity=".75" tb:tag="value-box-background"/> <rect x="21.5" y="362" width="56" height="30" rx="4" fill="#f3f3f3" fill-opacity=".75" tb:tag="value-box-background"/>
<rect x="22.5" y="363" width="54" height="28" rx="3" stroke="#fff" stroke-width="2"/> <rect x="22.5" y="363" width="54" height="28" rx="3" stroke="#fff" stroke-width="2"/>
<text x="50.492188" y="378.0625" fill="#727171" font-family="Roboto" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">37%</tspan></text> <text x="50.492188" y="378.0625" fill="#727171" font-family="Roboto, sans-serif" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">37%</tspan></text>
</g><path d="m37.56 0s-25.56 0-25.56 67v328.36c0 2.6512 0.68053 4.6404 1.52 4.6404h72.96c0.83942 0 1.52-1.9892 1.52-4.6404v-328.36c0-67-25.107-67-25.107-67h-12.893zm25.487 81.2c-0.48969 0-0.88669 1.2536-0.88669 2.8v300.4c0 1.5464 0.39697 2.8 0.88669 2.8h5.5733c0.48969 0 0.88669-1.2536 0.88669-2.8v-300.4c0-1.5464-0.39697-2.8-0.88669-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m37.56 0s-25.56 0-25.56 67v328.36c0 2.6512 0.68053 4.6404 1.52 4.6404h72.96c0.83942 0 1.52-1.9892 1.52-4.6404v-328.36c0-67-25.107-67-25.107-67h-12.893zm25.487 81.2c-0.48969 0-0.88669 1.2536-0.88669 2.8v300.4c0 1.5464 0.39697 2.8 0.88669 2.8h5.5733c0.48969 0 0.88669-1.2536 0.88669-2.8v-300.4c0-1.5464-0.39697-2.8-0.88669-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<filter id="filter0_ii_2475_365165" x="19.5" y="360" width="60" height="34" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_2475_365165" x="19.5" y="360" width="60" height="34" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

2
application/src/main/data/json/system/scada_symbols/pool.svg

@ -232,7 +232,7 @@
</g><g filter="url(#filter0_ii_2028_445065)" tb:tag="value-box"> </g><g filter="url(#filter0_ii_2028_445065)" tb:tag="value-box">
<path d="m1076 372.19c0-8.9433 6.8853-16.194 15.378-16.194h276.79c8.4922 0 15.377 7.251 15.377 16.194v75.573c0 8.9433-6.8852 16.194-15.377 16.194h-276.79c-8.4922 0-15.378-7.251-15.378-16.194z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m1076 372.19c0-8.9433 6.8853-16.194 15.378-16.194h276.79c8.4922 0 15.377 7.251 15.377 16.194v75.573c0 8.9433-6.8852 16.194-15.377 16.194h-276.79c-8.4922 0-15.378-7.251-15.378-16.194z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m1078 372.16c0-7.8336 6.0222-14.184 13.451-14.184h276.7c7.4286 0 13.451 6.3503 13.451 14.184v75.647c0 7.8336-6.0221 14.184-13.451 14.184h-276.7c-7.4285 0-13.451-6.3503-13.451-14.184z" stroke="#727171" stroke-width="3.9464"/> <path d="m1078 372.16c0-7.8336 6.0222-14.184 13.451-14.184h276.7c7.4286 0 13.451 6.3503 13.451 14.184v75.647c0 7.8336-6.0221 14.184-13.451 14.184h-276.7c-7.4285 0-13.451-6.3503-13.451-14.184z" stroke="#727171" stroke-width="3.9464"/>
<text x="1235.9021" y="410.52652" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="1235.9021" y="410.52652" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m2388 0c6.63 0 12 5.3726 12 12v15c0 6.6274-5.37 12-12 12h-2376c-6.6274 0-12-5.3726-12-12v-15c0-6.6274 5.3726-12 12-12h2376z" fill="#647484"/><path d="m2388 0c6.63 0 12 5.3726 12 12v15c0 6.6274-5.37 12-12 12h-2376c-6.6274 0-12-5.3726-12-12v-15c0-6.6274 5.3726-12 12-12h2376z" fill="url(#paint1846_linear_2028_445065)"/><path d="m2388 2c5.52 0 10 4.4772 10 10v15c0 5.5229-4.48 10-10 10h-2376c-5.523 0-10-4.4772-10-10v-15c0-5.5228 4.477-10 10-10h2376z" stroke="#000" stroke-opacity=".12" stroke-width="4"/><path d="m807.17 0s-807.17 0-807.17 134v656.72c0 5.3024 21.49 9.2807 48 9.2807h2304c26.508 0 48-3.9784 48-9.2807v-656.72c0-134-792.84-134-792.84-134h-407.16zm804.84 162.4c-15.464 0-28.001 2.5072-28.001 5.6v600.8c0 3.0928 12.536 5.6 28.001 5.6h176c15.464 0 28.001-2.5072 28.001-5.6v-600.8c0-3.0928-12.536-5.6-28.001-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m2388 0c6.63 0 12 5.3726 12 12v15c0 6.6274-5.37 12-12 12h-2376c-6.6274 0-12-5.3726-12-12v-15c0-6.6274 5.3726-12 12-12h2376z" fill="#647484"/><path d="m2388 0c6.63 0 12 5.3726 12 12v15c0 6.6274-5.37 12-12 12h-2376c-6.6274 0-12-5.3726-12-12v-15c0-6.6274 5.3726-12 12-12h2376z" fill="url(#paint1846_linear_2028_445065)"/><path d="m2388 2c5.52 0 10 4.4772 10 10v15c0 5.5229-4.48 10-10 10h-2376c-5.523 0-10-4.4772-10-10v-15c0-5.5228 4.477-10 10-10h2376z" stroke="#000" stroke-opacity=".12" stroke-width="4"/><path d="m807.17 0s-807.17 0-807.17 134v656.72c0 5.3024 21.49 9.2807 48 9.2807h2304c26.508 0 48-3.9784 48-9.2807v-656.72c0-134-792.84-134-792.84-134h-407.16zm804.84 162.4c-15.464 0-28.001 2.5072-28.001 5.6v600.8c0 3.0928 12.536 5.6 28.001 5.6h176c15.464 0 28.001-2.5072 28.001-5.6v-600.8c0-3.0928-12.536-5.6-28.001-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<filter id="filter0_ii_2028_445065" x="1076" y="356" width="308" height="108" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_2028_445065" x="1076" y="356" width="308" height="108" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

2
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

2
application/src/main/data/json/system/scada_symbols/right-heat-pump.svg

@ -584,7 +584,7 @@
</g><g filter="url(#filter2_ii_1826_356190)"> </g><g filter="url(#filter2_ii_1826_356190)">
<path d="m499 82c0-6.6274 5.373-12 12-12h157c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-157c-6.627 0-12-5.373-12-12v-56z" fill="#FFFEFE" tb:tag="value-box-background"/> <path d="m499 82c0-6.6274 5.373-12 12-12h157c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-157c-6.627 0-12-5.373-12-12v-56z" fill="#FFFEFE" tb:tag="value-box-background"/>
<path d="m500 82c0-6.0751 4.925-11 11-11h157c6.075 0 11 4.9249 11 11v56c0 6.075-4.925 11-11 11h-157c-6.075 0-11-4.925-11-11v-56z" stroke="#fff" stroke-width="2"/> <path d="m500 82c0-6.0751 4.925-11 11-11h157c6.075 0 11 4.9249 11 11v56c0 6.075-4.925 11-11 11h-157c-6.075 0-11-4.925-11-11v-56z" stroke="#fff" stroke-width="2"/>
<text x="589.51953" y="113.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">27</tspan></text> <text x="589.51953" y="113.75" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">27</tspan></text>
</g><circle cx="274" cy="278.2" r="180" fill="#B6B4B4"/><circle cx="274" cy="278.2" r="180" fill="url(#paint7_radial_1826_356190)"/><circle cx="274" cy="278.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(93.999 97.999)" tb:tag="fan"> </g><circle cx="274" cy="278.2" r="180" fill="#B6B4B4"/><circle cx="274" cy="278.2" r="180" fill="url(#paint7_radial_1826_356190)"/><circle cx="274" cy="278.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(93.999 97.999)" tb:tag="fan">
<circle cx="180" cy="180.2" r="180" fill="#b6b4b4"/> <circle cx="180" cy="180.2" r="180" fill="#b6b4b4"/>
<circle cx="180" cy="180.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <circle cx="180" cy="180.2" r="178.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

12
application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg

@ -621,32 +621,32 @@
<g tb:tag="filter"> <g tb:tag="filter">
<rect x="75" y="410" width="208.86" height="96.012" rx="9.8143" fill="#999"/> <rect x="75" y="410" width="208.86" height="96.012" rx="9.8143" fill="#999"/>
<path d="m76 419.81c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.8143-3.947-8.8143-8.815z" stroke="#999" stroke-width="2"/> <path d="m76 419.81c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.8143-3.947-8.8143-8.815z" stroke="#999" stroke-width="2"/>
<text x="180.79984" y="460.00018" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Filter</tspan></text> <text x="180.79984" y="460.00018" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Filter</tspan></text>
</g> </g>
<g tb:tag="backwash"> <g tb:tag="backwash">
<path d="m75 539.7c0-5.42 4.394-9.814 9.8143-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.421-4.394 9.815-9.814 9.815h-189.24c-5.4203 0-9.8143-4.394-9.8143-9.815z" fill="#fff"/> <path d="m75 539.7c0-5.42 4.394-9.814 9.8143-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.421-4.394 9.815-9.814 9.815h-189.24c-5.4203 0-9.8143-4.394-9.8143-9.815z" fill="#fff"/>
<path d="m76 539.7c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.8143-3.947-8.8143-8.815z" stroke="#999" stroke-width="2"/> <path d="m76 539.7c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.8143-3.947-8.8143-8.815z" stroke="#999" stroke-width="2"/>
<text x="183.43652" y="579.61719" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Backwash</tspan></text> <text x="183.43652" y="579.61719" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Backwash</tspan></text>
</g> </g>
<g tb:tag="rinse"> <g tb:tag="rinse">
<path d="m75 659.58c0-5.42 4.394-9.814 9.8143-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.384c0 5.42-4.394 9.814-9.814 9.814h-189.24c-5.4203 0-9.8143-4.394-9.8143-9.814z" fill="#fff"/> <path d="m75 659.58c0-5.42 4.394-9.814 9.8143-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.384c0 5.42-4.394 9.814-9.814 9.814h-189.24c-5.4203 0-9.8143-4.394-9.8143-9.814z" fill="#fff"/>
<path d="m76 659.58c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.384c0 4.868-3.946 8.814-8.814 8.814h-189.24c-4.868 0-8.8143-3.946-8.8143-8.814z" stroke="#999" stroke-width="2"/> <path d="m76 659.58c0-4.868 3.9463-8.814 8.8143-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.384c0 4.868-3.946 8.814-8.814 8.814h-189.24c-4.868 0-8.8143-3.946-8.8143-8.814z" stroke="#999" stroke-width="2"/>
<text x="181.64941" y="699.61719" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Rinse</tspan></text> <text x="181.64941" y="699.61719" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Rinse</tspan></text>
</g> </g>
<g tb:tag="waste"> <g tb:tag="waste">
<path d="m315.69 419.81c0-5.42 4.394-9.814 9.814-9.814h189.24c5.42 0 9.815 4.394 9.815 9.814v76.383c0 5.421-4.395 9.815-9.815 9.815h-189.24c-5.42 0-9.814-4.394-9.814-9.814z" fill="#fff"/> <path d="m315.69 419.81c0-5.42 4.394-9.814 9.814-9.814h189.24c5.42 0 9.815 4.394 9.815 9.814v76.383c0 5.421-4.395 9.815-9.815 9.815h-189.24c-5.42 0-9.814-4.394-9.814-9.814z" fill="#fff"/>
<path d="m316.69 419.81c0-4.868 3.946-8.814 8.814-8.814h189.24c4.868 0 8.815 3.946 8.815 8.814v76.383c0 4.869-3.947 8.815-8.815 8.815h-189.24c-4.868 0-8.814-3.946-8.814-8.814z" stroke="#999" stroke-width="2"/> <path d="m316.69 419.81c0-4.868 3.946-8.814 8.814-8.814h189.24c4.868 0 8.815 3.946 8.815 8.814v76.383c0 4.869-3.947 8.815-8.815 8.815h-189.24c-4.868 0-8.814-3.946-8.814-8.814z" stroke="#999" stroke-width="2"/>
<text x="421.99316" y="459.75586" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Waste</tspan></text> <text x="421.99316" y="459.75586" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Waste</tspan></text>
</g> </g>
<g tb:tag="recirculate"> <g tb:tag="recirculate">
<path d="m315.69 539.7c0-5.42 4.394-9.814 9.815-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.421-4.394 9.815-9.814 9.815h-189.24c-5.421 0-9.815-4.394-9.815-9.815z" fill="#fff"/> <path d="m315.69 539.7c0-5.42 4.394-9.814 9.815-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.421-4.394 9.815-9.814 9.815h-189.24c-5.421 0-9.815-4.394-9.815-9.815z" fill="#fff"/>
<path d="m316.69 539.7c0-4.868 3.947-8.814 8.815-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.815-3.947-8.815-8.815z" stroke="#999" stroke-width="2"/> <path d="m316.69 539.7c0-4.868 3.947-8.814 8.815-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.815-8.814 8.815h-189.24c-4.868 0-8.815-3.947-8.815-8.815z" stroke="#999" stroke-width="2"/>
<text x="425.14062" y="579.61719" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Recirculate</tspan></text> <text x="425.14062" y="579.61719" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Recirculate</tspan></text>
</g> </g>
<g tb:tag="closed"> <g tb:tag="closed">
<path d="m315.69 659.58c0-5.42 4.394-9.814 9.815-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.42-4.394 9.814-9.814 9.814h-189.24c-5.421 0-9.815-4.394-9.815-9.814z" fill="#fff"/> <path d="m315.69 659.58c0-5.42 4.394-9.814 9.815-9.814h189.24c5.42 0 9.814 4.394 9.814 9.814v76.383c0 5.42-4.394 9.814-9.814 9.814h-189.24c-5.421 0-9.815-4.394-9.815-9.814z" fill="#fff"/>
<path d="m316.69 659.58c0-4.868 3.947-8.814 8.815-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.814-8.814 8.814h-189.24c-4.868 0-8.815-3.946-8.815-8.814z" stroke="#999" stroke-width="2"/> <path d="m316.69 659.58c0-4.868 3.947-8.814 8.815-8.814h189.24c4.868 0 8.814 3.946 8.814 8.814v76.383c0 4.868-3.946 8.814-8.814 8.814h-189.24c-4.868 0-8.815-3.946-8.815-8.814z" stroke="#999" stroke-width="2"/>
<text x="422.45215" y="699.61719" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Closed</tspan></text> <text x="422.45215" y="699.61719" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Closed</tspan></text>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

12
application/src/main/data/json/system/scada_symbols/sand-filter.svg

@ -408,37 +408,37 @@
<path d="m74 470c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m74 470c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m75.5 470c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#198038" stroke-width="3"/> <path d="m75.5 470c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#198038" stroke-width="3"/>
<circle cx="179.5" cy="483" r="10" fill="#198038"/> <circle cx="179.5" cy="483" r="10" fill="#198038"/>
<text x="178.02383" y="512.61719" fill="#000000" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Filter</tspan></text> <text x="178.02383" y="512.61719" fill="#000000" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Filter</tspan></text>
</g> </g>
<g filter="url(#filter1_ii_1830_321985)"> <g filter="url(#filter1_ii_1830_321985)">
<path d="m74 574c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m74 574c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m75.5 574c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <path d="m75.5 574c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<circle cx="179.5" cy="587" r="10" fill="#000" fill-opacity=".12"/> <circle cx="179.5" cy="587" r="10" fill="#000" fill-opacity=".12"/>
<text x="179.43652" y="617.61719" fill="#727272" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Backwash</tspan></text> <text x="179.43652" y="617.61719" fill="#727272" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Backwash</tspan></text>
</g> </g>
<g filter="url(#filter2_ii_1830_321985)"> <g filter="url(#filter2_ii_1830_321985)">
<path d="m74 678c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m74 678c0-6.627 5.3726-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.6274 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m75.5 678c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <path d="m75.5 678c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<circle cx="179" cy="691" r="10" fill="#000" fill-opacity=".12"/> <circle cx="179" cy="691" r="10" fill="#000" fill-opacity=".12"/>
<text x="177.64941" y="722.61719" fill="#727272" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Rinse</tspan></text> <text x="177.64941" y="722.61719" fill="#727272" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Rinse</tspan></text>
</g> </g>
<g filter="url(#filter3_ii_1830_321985)"> <g filter="url(#filter3_ii_1830_321985)">
<path d="m316 470c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m316 470c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m317.5 470c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <path d="m317.5 470c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<circle cx="421" cy="483" r="10" fill="#000" fill-opacity=".12"/> <circle cx="421" cy="483" r="10" fill="#000" fill-opacity=".12"/>
<text x="420.99316" y="512.75586" fill="#727272" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Waste</tspan></text> <text x="420.99316" y="512.75586" fill="#727272" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Waste</tspan></text>
</g> </g>
<g filter="url(#filter4_ii_1830_321985)"> <g filter="url(#filter4_ii_1830_321985)">
<path d="m316 574c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m316 574c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m317.5 574c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <path d="m317.5 574c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<circle cx="421.5" cy="587" r="10" fill="#000" fill-opacity=".12"/> <circle cx="421.5" cy="587" r="10" fill="#000" fill-opacity=".12"/>
<text x="420.14062" y="617.61719" fill="#727272" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Recirculate</tspan></text> <text x="420.14062" y="617.61719" fill="#727272" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Recirculate</tspan></text>
</g> </g>
<g filter="url(#filter5_ii_1830_321985)"> <g filter="url(#filter5_ii_1830_321985)">
<path d="m316 678c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/> <path d="m316 678c0-6.627 5.373-12 12-12h186c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-186c-6.627 0-12-5.373-12-12v-56z" fill="#fff"/>
<path d="m317.5 678c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/> <path d="m317.5 678c0-5.799 4.701-10.5 10.5-10.5h186c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-186c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#000" stroke-opacity=".12" stroke-width="3"/>
<circle cx="421.5" cy="691" r="10" fill="#000" fill-opacity=".12"/> <circle cx="421.5" cy="691" r="10" fill="#000" fill-opacity=".12"/>
<text x="421.45215" y="721.61719" fill="#727272" font-family="Roboto" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Closed</tspan></text> <text x="421.45215" y="721.61719" fill="#727272" font-family="Roboto, sans-serif" font-size="28px" font-weight="500" text-anchor="middle" xml:space="preserve"><tspan dominant-baseline="middle">Closed</tspan></text>
</g> </g>
</g> </g>
<defs> <defs>

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

4
application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg

@ -490,13 +490,13 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<text x="409.16602" y="97.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="401.15625" y="345.75" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g fill="#666" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" style="display: none;" tb:tag="minMaxValue"> <text x="409.16602" y="97.234375" fill="#000000" font-family="Roboto, sans-serif" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="401.15625" y="345.75" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g fill="#666" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" text-anchor="middle" style="display: none;" tb:tag="minMaxValue">
<text x="88.070312" y="268" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text> <text x="88.070312" y="268" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text>
<text x="705.02344" y="268" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text> <text x="705.02344" y="268" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text>
</g><g transform="translate(180)" tb:tag="valueArrowPosition"> </g><g transform="translate(180)" tb:tag="valueArrowPosition">
<path d="m80 179 24-42h-48z" fill="#666" tb:tag="valuePointer"/> <path d="m80 179 24-42h-48z" fill="#666" tb:tag="valuePointer"/>
</g><g transform="translate(180)" tb:tag="valueTextPositon"> </g><g transform="translate(180)" tb:tag="valueTextPositon">
<text x="79.779297" y="273.125" fill="#002878" font-family="Roboto" font-size="60px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">26</tspan></text> <text x="79.779297" y="273.125" fill="#002878" font-family="Roboto, sans-serif" font-size="60px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">26</tspan></text>
</g><g stroke="#000" tb:tag="scale"> </g><g stroke="#000" tb:tag="scale">
<rect x="80.5" y="183.5" width="652" height="41" fill="#c8dff7" tb:tag="scaleBackground"/> <rect x="80.5" y="183.5" width="652" height="41" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect x="80.5" y="183.5" width="164" height="41" fill="#ebebeb" tb:tag="lowWarningScale"/> <rect x="80.5" y="183.5" width="164" height="41" fill="#ebebeb" tb:tag="lowWarningScale"/>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

6
application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg

@ -490,19 +490,19 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale"> <text x="206.16602" y="43.234375" fill="#000000" font-family="Roboto, sans-serif" font-size="56px" font-weight="400" text-anchor="middle" tb:tag="label" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">Outdoor</tspan></text><text x="195.21875" y="770" fill="#000000" font-family="Roboto, sans-serif" font-size="40px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan transform="translate(0,-144)" dominant-baseline="middle">°C</tspan></text><g stroke="#000" tb:tag="scale">
<rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/> <rect x="176.5" y="83" width="41" height="652" fill="#c8dff7" tb:tag="scaleBackground"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/> <rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="164" fill="#ebebeb" tb:tag="lowWarningScale"/>
<rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/> <rect x="176.5" y="83.5" width="41" height="164" fill="#ebebeb" tb:tag="highWarningScale"/>
<rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="81" fill="#666" tb:tag="lowCriticalScale"/> <rect transform="scale(1,-1)" x="176.5" y="-735" width="41" height="81" fill="#666" tb:tag="lowCriticalScale"/>
<rect x="176.5" y="83.5" width="41" height="81" fill="#666" tb:tag="highCriticalScale"/> <rect x="176.5" y="83.5" width="41" height="81" fill="#666" tb:tag="highCriticalScale"/>
</g><g fill="#666" font-family="Roboto" font-size="32px" font-weight="500" style="display: none;" tb:tag="minMaxValue"> </g><g fill="#666" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" style="display: none;" tb:tag="minMaxValue">
<text x="237.48438" y="97" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text> <text x="237.48438" y="97" tb:tag="maxValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">100</tspan></text>
<text x="238.89062" y="727" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text> <text x="238.89062" y="727" tb:tag="minValue" xml:space="preserve" style=""><tspan dominant-baseline="middle">0</tspan></text>
</g><g transform="translate(0,-180)" tb:tag="valueArrowPosition"> </g><g transform="translate(0,-180)" tb:tag="valueArrowPosition">
<path d="m168 735-42-24v48z" fill="#666" tb:tag="valuePointer"/> <path d="m168 735-42-24v48z" fill="#666" tb:tag="valuePointer"/>
</g><g transform="translate(0,-180)" tb:tag="valueTextPositon"> </g><g transform="translate(0,-180)" tb:tag="valueTextPositon">
<text x="223.60547" y="739.125" fill="#002878" font-family="Roboto" font-size="60px" font-weight="400" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="middle">26</tspan></text> <text x="223.60547" y="739.125" fill="#002878" font-family="Roboto, sans-serif" font-size="60px" font-weight="400" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="middle">26</tspan></text>
</g><g transform="translate(0,-400)" style="display: none;" tb:tag="target"> </g><g transform="translate(0,-400)" style="display: none;" tb:tag="target">
<rect transform="rotate(-45)" x="-379.76" y="636.31" width="22" height="22" fill="#dedede" stroke="#000" stroke-width="2" tb:tag="targetBackground" style=""/> <rect transform="rotate(-45)" x="-379.76" y="636.31" width="22" height="22" fill="#dedede" stroke="#000" stroke-width="2" tb:tag="targetBackground" style=""/>
</g><path d="m134.53 0s-134.53 0-134.53 134v656.72c0 5.3024 3.5817 9.2808 8 9.2808h384c4.418 0 8-3.9784 8-9.2808v-656.72c0-134-132.14-134-132.14-134h-67.86zm134.14 162.4c-2.5773 0-4.6667 2.5072-4.6667 5.6v600.8c0 3.0928 2.0893 5.6 4.6667 5.6h29.333c2.5773 0 4.6667-2.5072 4.6667-5.6v-600.8c0-3.0928-2.0893-5.6-4.6667-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/> </g><path d="m134.53 0s-134.53 0-134.53 134v656.72c0 5.3024 3.5817 9.2808 8 9.2808h384c4.418 0 8-3.9784 8-9.2808v-656.72c0-134-132.14-134-132.14-134h-67.86zm134.14 162.4c-2.5773 0-4.6667 2.5072-4.6667 5.6v600.8c0 3.0928 2.0893 5.6 4.6667 5.6h29.333c2.5773 0 4.6667-2.5072 4.6667-5.6v-600.8c0-3.0928-2.0893-5.6-4.6667-5.6z" fill="#000" fill-opacity="0" tb:tag="clickArea"/>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

2
application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg

@ -534,7 +534,7 @@
</g><g transform="translate(-201,6)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </g><g transform="translate(-201,6)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m201.79 0s-201.79 0-201.79 100.5v492.54c0 3.9768 5.3726 6.9606 12 6.9606h576c6.627 0 12-2.9838 12-6.9606v-492.54c0-100.5-198.21-100.5-198.21-100.5h-101.79zm201.21 121.8c-3.866 0-7 1.8804-7 4.2v450.6c0 2.3196 3.134 4.2 7 4.2h44c3.866 0 7-1.8804 7-4.2v-450.6c0-2.3196-3.134-4.2-7-4.2z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m201.79 0s-201.79 0-201.79 100.5v492.54c0 3.9768 5.3726 6.9606 12 6.9606h576c6.627 0 12-2.9838 12-6.9606v-492.54c0-100.5-198.21-100.5-198.21-100.5h-101.79zm201.21 121.8c-3.866 0-7 1.8804-7 4.2v450.6c0 2.3196 3.134 4.2 7 4.2h44c3.866 0 7-1.8804 7-4.2v-450.6c0-2.3196-3.134-4.2-7-4.2z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<pattern id="pattern17033" patternTransform="translate(-79.813 574.69)" xlink:href="#liquid"/> <pattern id="pattern17033" patternTransform="translate(-79.813 574.69)" xlink:href="#liquid"/>
<linearGradient id="paint0_linear_2901_184349" x1="600" x2=".019179" y1="110.96" y2="104.56" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_2901_184349" x1="600" x2=".019179" y1="110.96" y2="104.56" gradientUnits="userSpaceOnUse">

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

2
application/src/main/data/json/system/scada_symbols/small-left-meter.svg

@ -720,6 +720,6 @@
</g><g transform="translate(-12)" filter="url(#filter0_ii_3742_299961)" style="display: none;" tb:tag="value-box"> </g><g transform="translate(-12)" filter="url(#filter0_ii_3742_299961)" style="display: none;" tb:tag="value-box">
<rect x="22" y="164" width="56" height="28" rx="4" fill="#fffefe" fill-opacity=".75" tb:tag="value-box-background" style=""/> <rect x="22" y="164" width="56" height="28" rx="4" fill="#fffefe" fill-opacity=".75" tb:tag="value-box-background" style=""/>
<rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2" style=""/> <rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2" style=""/>
<text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve" style=""><tspan dominant-baseline="middle">37%</tspan></text> <text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto, sans-serif" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve" style=""><tspan dominant-baseline="middle">37%</tspan></text>
</g><path d="m25.56-0.12402s-25.56 0-25.56 33.5v164.18c0 1.3256 0.68053 2.3202 1.52 2.3202h72.96c0.83942 0 1.52-0.9946 1.52-2.3202v-164.18c0-33.5-25.107-33.5-25.107-33.5h-12.893zm25.487 40.6c-0.48969 0-0.88669 0.6268-0.88669 1.4v150.2c0 0.7732 0.39697 1.4 0.88669 1.4h5.5733c0.48969 0 0.88669-0.6268 0.88669-1.4v-150.2c0-0.7732-0.39697-1.4-0.88669-1.4z" fill-opacity="0" fill="#000" tb:tag="clickArea"/> </g><path d="m25.56-0.12402s-25.56 0-25.56 33.5v164.18c0 1.3256 0.68053 2.3202 1.52 2.3202h72.96c0.83942 0 1.52-0.9946 1.52-2.3202v-164.18c0-33.5-25.107-33.5-25.107-33.5h-12.893zm25.487 40.6c-0.48969 0-0.88669 0.6268-0.88669 1.4v150.2c0 0.7732 0.39697 1.4 0.88669 1.4h5.5733c0.48969 0 0.88669-0.6268 0.88669-1.4v-150.2c0-0.7732-0.39697-1.4-0.88669-1.4z" fill-opacity="0" fill="#000" tb:tag="clickArea"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

2
application/src/main/data/json/system/scada_symbols/small-meter.svg

@ -657,7 +657,7 @@
</g><g filter="url(#filter0_ii_3742_299961)" tb:tag="value-box"> </g><g filter="url(#filter0_ii_3742_299961)" tb:tag="value-box">
<rect x="22" y="164" width="56" height="28" rx="4" fill="#FFFEFE" fill-opacity=".75" tb:tag="value-box-background"/> <rect x="22" y="164" width="56" height="28" rx="4" fill="#FFFEFE" fill-opacity=".75" tb:tag="value-box-background"/>
<rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2"/> <rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2"/>
<text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">37%</tspan></text> <text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto, sans-serif" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan dominant-baseline="middle">37%</tspan></text>
</g><g tb:tag="progressBar"> </g><g tb:tag="progressBar">
<rect x="69" y="10.5" width="6" height="134" ry="1.12" stroke="#cecece" tb:tag="progressBorder"/> <rect x="69" y="10.5" width="6" height="134" ry="1.12" stroke="#cecece" tb:tag="progressBorder"/>
<rect transform="scale(1,-1)" x="69" y="-144.5" width="6" height="50" ry="1.12" fill="#4d94e1" stroke="#4d94e1" tb:tag="progressFill"/> <rect transform="scale(1,-1)" x="69" y="-144.5" width="6" height="50" ry="1.12" fill="#4d94e1" stroke="#4d94e1" tb:tag="progressFill"/>

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

2
application/src/main/data/json/system/scada_symbols/small-right-center.svg

@ -669,7 +669,7 @@
</g><g transform="translate(12)" filter="url(#filter0_ii_3742_299961)" style="display: none;" tb:tag="value-box"> </g><g transform="translate(12)" filter="url(#filter0_ii_3742_299961)" style="display: none;" tb:tag="value-box">
<rect x="22" y="164" width="56" height="28" rx="4" fill="#fffefe" fill-opacity=".75" tb:tag="value-box-background" style=""/> <rect x="22" y="164" width="56" height="28" rx="4" fill="#fffefe" fill-opacity=".75" tb:tag="value-box-background" style=""/>
<rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2" style=""/> <rect x="23" y="165" width="54" height="26" rx="3" stroke="#fff" stroke-width="2" style=""/>
<text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve" style=""><tspan dominant-baseline="middle">37%</tspan></text> <text x="49.853027" y="179.5625" fill="#727171" font-family="Roboto, sans-serif" font-size="14px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve" style=""><tspan dominant-baseline="middle">37%</tspan></text>
</g><path d="m49.56 0s-25.56 0-25.56 33.5v164.18c0 1.3256 0.68053 2.3202 1.52 2.3202h72.96c0.83942 0 1.52-0.9946 1.52-2.3202v-164.18c0-33.5-25.107-33.5-25.107-33.5h-12.893zm25.487 40.6c-0.48969 0-0.88669 0.6268-0.88669 1.4v150.2c0 0.7732 0.39697 1.4 0.88669 1.4h5.5733c0.48969 0 0.88669-0.6268 0.88669-1.4v-150.2c0-0.7732-0.39697-1.4-0.88669-1.4z" fill-opacity="0" tb:tag="clickArea"/><path d="m8 200c2.2091 0 4-1.791 4-4v-192c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v192c0 2.209 1.7909 4 4 4h4z" fill="#93979B"/><path d="m8 200c2.2091 0 4-1.791 4-4v-192c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v192c0 2.209 1.7909 4 4 4h4z" fill="url(#paint1_linear_3742_299973)"/><path d="m8 198.5c1.3807 0 2.5-1.119 2.5-2.5v-192c0-1.3807-1.1193-2.5-2.5-2.5h-4c-1.3807 0-2.5 1.1193-2.5 2.5v192c0 1.381 1.1193 2.5 2.5 2.5h4z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m12 146c0 2.209 1.7908 4 3.9999 4h4c2.2092 0 4.0001-1.791 4.0001-4v-92c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v92z" fill="#647484"/><path d="m12 146c0 2.209 1.7908 4 3.9999 4h4c2.2092 0 4.0001-1.791 4.0001-4v-92c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v92z" fill="url(#paint2_linear_3742_299973)"/><path d="m13.5 146c0 1.38 1.1193 2.5 2.4999 2.5h4c1.3808 0 2.5001-1.119 2.5001-2.5v-92c0-1.3807-1.1193-2.5-2.5-2.5h-4c-1.3807 0-2.5 1.1193-2.5 2.5v92z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs> </g><path d="m49.56 0s-25.56 0-25.56 33.5v164.18c0 1.3256 0.68053 2.3202 1.52 2.3202h72.96c0.83942 0 1.52-0.9946 1.52-2.3202v-164.18c0-33.5-25.107-33.5-25.107-33.5h-12.893zm25.487 40.6c-0.48969 0-0.88669 0.6268-0.88669 1.4v150.2c0 0.7732 0.39697 1.4 0.88669 1.4h5.5733c0.48969 0 0.88669-0.6268 0.88669-1.4v-150.2c0-0.7732-0.39697-1.4-0.88669-1.4z" fill-opacity="0" tb:tag="clickArea"/><path d="m8 200c2.2091 0 4-1.791 4-4v-192c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v192c0 2.209 1.7909 4 4 4h4z" fill="#93979B"/><path d="m8 200c2.2091 0 4-1.791 4-4v-192c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v192c0 2.209 1.7909 4 4 4h4z" fill="url(#paint1_linear_3742_299973)"/><path d="m8 198.5c1.3807 0 2.5-1.119 2.5-2.5v-192c0-1.3807-1.1193-2.5-2.5-2.5h-4c-1.3807 0-2.5 1.1193-2.5 2.5v192c0 1.381 1.1193 2.5 2.5 2.5h4z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m12 146c0 2.209 1.7908 4 3.9999 4h4c2.2092 0 4.0001-1.791 4.0001-4v-92c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v92z" fill="#647484"/><path d="m12 146c0 2.209 1.7908 4 3.9999 4h4c2.2092 0 4.0001-1.791 4.0001-4v-92c0-2.2091-1.7909-4-4-4h-4c-2.2091 0-4 1.7909-4 4v92z" fill="url(#paint2_linear_3742_299973)"/><path d="m13.5 146c0 1.38 1.1193 2.5 2.4999 2.5h4c1.3808 0 2.5001-1.119 2.5001-2.5v-92c0-1.3807-1.1193-2.5-2.5-2.5h-4c-1.3807 0-2.5 1.1193-2.5 2.5v92z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs>
<linearGradient id="paint0_linear_3742_299973" x1="100" x2="24" y1="100" y2="100.09" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_3742_299973" x1="100" x2="24" y1="100" y2="100.09" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/> <stop stop-color="#020202" stop-opacity=".35" offset="0"/>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

2
application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg

@ -539,7 +539,7 @@
</g><rect x="79" y="563" width="80" height="24" rx="7" fill="#fff"/><rect x="79" y="563" width="80" height="24" rx="7" fill="url(#paint145_linear_1711_268272)"/><rect x="80.5" y="564.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="441" y="563" width="80" height="24" rx="7" fill="#fff"/><rect x="441" y="563" width="80" height="24" rx="7" fill="url(#paint146_linear_1711_268272)"/><rect x="442.5" y="564.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m103 563v-38l32 25v13h-32z" fill="#fff"/><path d="m103 563v-38l32 25v13h-32z" fill="url(#paint147_linear_1711_268272)"/><path d="m104.5 561.5v-33.425l29 22.657v10.768h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m465 563v-13l32-24v37h-32z" fill="#fff"/><path d="m465 563v-13l32-24v37h-32z" fill="url(#paint148_linear_1711_268272)"/><path d="m466.5 561.5v-10.75l29-21.75v32.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="513" cy="406" r="32" fill="url(#paint149_radial_1711_268272)"/><circle cx="513" cy="406" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="87" cy="406" r="32" fill="url(#paint150_radial_1711_268272)"/><circle cx="87" cy="406" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m71 571v-165c0-8.837 7.1634-16 16-16s16 7.163 16 16v165h-32z" fill="#fff"/><path d="m71 571v-165c0-8.837 7.1634-16 16-16s16 7.163 16 16v165h-32z" fill="url(#paint151_linear_1711_268272)"/><path d="m72.5 569.5v-163.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v163.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m497 571v-165c0-8.837 7.163-16 16-16s16 7.163 16 16v165h-32z" fill="#fff"/><path d="m497 571v-165c0-8.837 7.163-16 16-16s16 7.163 16 16v165h-32z" fill="url(#paint152_linear_1711_268272)"/><path d="m498.5 569.5v-163.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v163.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="37" y="571" width="100" height="29" rx="7" fill="#fff"/><rect x="37" y="571" width="100" height="29" rx="7" fill="url(#paint153_linear_1711_268272)"/><rect x="38.5" y="572.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="463" y="571" width="100" height="29" rx="7" fill="#fff"/><rect x="463" y="571" width="100" height="29" rx="7" fill="url(#paint154_linear_1711_268272)"/><rect x="464.5" y="572.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(-201)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </g><rect x="79" y="563" width="80" height="24" rx="7" fill="#fff"/><rect x="79" y="563" width="80" height="24" rx="7" fill="url(#paint145_linear_1711_268272)"/><rect x="80.5" y="564.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="441" y="563" width="80" height="24" rx="7" fill="#fff"/><rect x="441" y="563" width="80" height="24" rx="7" fill="url(#paint146_linear_1711_268272)"/><rect x="442.5" y="564.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m103 563v-38l32 25v13h-32z" fill="#fff"/><path d="m103 563v-38l32 25v13h-32z" fill="url(#paint147_linear_1711_268272)"/><path d="m104.5 561.5v-33.425l29 22.657v10.768h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m465 563v-13l32-24v37h-32z" fill="#fff"/><path d="m465 563v-13l32-24v37h-32z" fill="url(#paint148_linear_1711_268272)"/><path d="m466.5 561.5v-10.75l29-21.75v32.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="513" cy="406" r="32" fill="url(#paint149_radial_1711_268272)"/><circle cx="513" cy="406" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="87" cy="406" r="32" fill="url(#paint150_radial_1711_268272)"/><circle cx="87" cy="406" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m71 571v-165c0-8.837 7.1634-16 16-16s16 7.163 16 16v165h-32z" fill="#fff"/><path d="m71 571v-165c0-8.837 7.1634-16 16-16s16 7.163 16 16v165h-32z" fill="url(#paint151_linear_1711_268272)"/><path d="m72.5 569.5v-163.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v163.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m497 571v-165c0-8.837 7.163-16 16-16s16 7.163 16 16v165h-32z" fill="#fff"/><path d="m497 571v-165c0-8.837 7.163-16 16-16s16 7.163 16 16v165h-32z" fill="url(#paint152_linear_1711_268272)"/><path d="m498.5 569.5v-163.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v163.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="37" y="571" width="100" height="29" rx="7" fill="#fff"/><rect x="37" y="571" width="100" height="29" rx="7" fill="url(#paint153_linear_1711_268272)"/><rect x="38.5" y="572.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="463" y="571" width="100" height="29" rx="7" fill="#fff"/><rect x="463" y="571" width="100" height="29" rx="7" fill="url(#paint154_linear_1711_268272)"/><rect x="464.5" y="572.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(-201)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="500.74219" y="300.07812" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m201.79 0s-201.79 0-201.79 100.5v492.54c0 3.9768 5.3726 6.9605 12 6.9605h576c6.627 0 12-2.9838 12-6.9605v-492.54c0-100.5-198.21-100.5-198.21-100.5h-101.79zm201.21 121.8c-3.866 0-7.0002 1.8804-7.0002 4.2v450.6c0 2.3196 3.134 4.2 7.0002 4.2h44c3.866 0 7.0002-1.8804 7.0002-4.2v-450.6c0-2.3196-3.134-4.2-7.0002-4.2z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m201.79 0s-201.79 0-201.79 100.5v492.54c0 3.9768 5.3726 6.9605 12 6.9605h576c6.627 0 12-2.9838 12-6.9605v-492.54c0-100.5-198.21-100.5-198.21-100.5h-101.79zm201.21 121.8c-3.866 0-7.0002 1.8804-7.0002 4.2v450.6c0 2.3196 3.134 4.2 7.0002 4.2h44c3.866 0 7.0002-1.8804 7.0002-4.2v-450.6c0-2.3196-3.134-4.2-7.0002-4.2z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<radialGradient id="paint0_radial_1711_268272" cx="0" cy="0" r="1" gradientTransform="translate(300 300.7) rotate(180.27) scale(300 300.7)" gradientUnits="userSpaceOnUse"> <radialGradient id="paint0_radial_1711_268272" cx="0" cy="0" r="1" gradientTransform="translate(300 300.7) rotate(180.27) scale(300 300.7)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff" stop-opacity=".15" offset=".00034187"/> <stop stop-color="#fff" stop-opacity=".15" offset=".00034187"/>

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

2
application/src/main/data/json/system/scada_symbols/spherical-tank.svg

@ -569,7 +569,7 @@
</g><rect x="182" y="963" width="80" height="24" rx="7" fill="#fff"/><rect x="182" y="963" width="80" height="24" rx="7" fill="url(#paint245_linear_1711_251491)"/><rect x="183.5" y="964.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="738" y="963" width="80" height="24" rx="7" fill="#fff"/><rect x="738" y="963" width="80" height="24" rx="7" fill="url(#paint246_linear_1711_251491)"/><rect x="739.5" y="964.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m206 963v-59l32 22v37h-32z" fill="#fff"/><path d="m206 963v-59l32 22v37h-32z" fill="url(#paint247_linear_1711_251491)"/><path d="m207.5 961.5v-54.648l29 19.937v34.711h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m762 963v-37l32-22v59h-32z" fill="#fff"/><path d="m762 963v-37l32-22v59h-32z" fill="url(#paint248_linear_1711_251491)"/><path d="m763.5 961.5v-34.711l29-19.937v54.648h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="810" cy="788" r="32" fill="url(#paint249_radial_1711_251491)"/><circle cx="810" cy="788" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="190" cy="788" r="32" fill="url(#paint250_radial_1711_251491)"/><circle cx="190" cy="788" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m174 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="#fff"/><path d="m174 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="url(#paint251_linear_1711_251491)"/><path d="m175.5 969.5v-181.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v181.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m794 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="#fff"/><path d="m794 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="url(#paint252_linear_1711_251491)"/><path d="m795.5 969.5v-181.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v181.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="140" y="971" width="100" height="29" rx="7" fill="#fff"/><rect x="140" y="971" width="100" height="29" rx="7" fill="url(#paint253_linear_1711_251491)"/><rect x="141.5" y="972.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="760" y="971" width="100" height="29" rx="7" fill="#fff"/><rect x="760" y="971" width="100" height="29" rx="7" fill="url(#paint254_linear_1711_251491)"/><rect x="761.5" y="972.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(-1.6394 200)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </g><rect x="182" y="963" width="80" height="24" rx="7" fill="#fff"/><rect x="182" y="963" width="80" height="24" rx="7" fill="url(#paint245_linear_1711_251491)"/><rect x="183.5" y="964.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="738" y="963" width="80" height="24" rx="7" fill="#fff"/><rect x="738" y="963" width="80" height="24" rx="7" fill="url(#paint246_linear_1711_251491)"/><rect x="739.5" y="964.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m206 963v-59l32 22v37h-32z" fill="#fff"/><path d="m206 963v-59l32 22v37h-32z" fill="url(#paint247_linear_1711_251491)"/><path d="m207.5 961.5v-54.648l29 19.937v34.711h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m762 963v-37l32-22v59h-32z" fill="#fff"/><path d="m762 963v-37l32-22v59h-32z" fill="url(#paint248_linear_1711_251491)"/><path d="m763.5 961.5v-34.711l29-19.937v54.648h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="810" cy="788" r="32" fill="url(#paint249_radial_1711_251491)"/><circle cx="810" cy="788" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="190" cy="788" r="32" fill="url(#paint250_radial_1711_251491)"/><circle cx="190" cy="788" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m174 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="#fff"/><path d="m174 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="url(#paint251_linear_1711_251491)"/><path d="m175.5 969.5v-181.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v181.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m794 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="#fff"/><path d="m794 971v-183c0-8.837 7.163-16 16-16s16 7.163 16 16v183h-32z" fill="url(#paint252_linear_1711_251491)"/><path d="m795.5 969.5v-181.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v181.5h-29z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="140" y="971" width="100" height="29" rx="7" fill="#fff"/><rect x="140" y="971" width="100" height="29" rx="7" fill="url(#paint253_linear_1711_251491)"/><rect x="141.5" y="972.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="760" y="971" width="100" height="29" rx="7" fill="#fff"/><rect x="760" y="971" width="100" height="29" rx="7" fill="url(#paint254_linear_1711_251491)"/><rect x="761.5" y="972.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><g transform="translate(-1.6394 200)" filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><path d="m336.32 0s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><path d="m336.32 0s-336.32 0-336.32 167.5v820.9c0 6.628 8.9543 11.601 20 11.601h960c11.045 0 20-4.973 20-11.601v-820.9c0-167.5-330.35-167.5-330.35-167.5h-169.65zm335.35 203c-6.4433 0-11.667 3.134-11.667 7v751c0 3.866 5.2233 7 11.667 7h73.333c6.4433 0 11.667-3.134 11.667-7v-751c0-3.866-5.2233-7-11.667-7z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<radialGradient id="paint0_radial_1711_251491" cx="0" cy="0" r="1" gradientTransform="translate(500 501.16) rotate(180.27) scale(500.01 501.17)" gradientUnits="userSpaceOnUse"> <radialGradient id="paint0_radial_1711_251491" cx="0" cy="0" r="1" gradientTransform="translate(500 501.16) rotate(180.27) scale(500.01 501.17)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff" stop-opacity=".15" offset=".00034187"/> <stop stop-color="#fff" stop-opacity=".15" offset=".00034187"/>

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

2
application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg

@ -564,7 +564,7 @@
</g><g transform="translate(-1,409)" filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </g><g transform="translate(-1,409)" filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><ellipse cx="300" cy="16" rx="292" ry="16" fill="#5D5C5C"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="url(#paint147_linear_1690_149725)" style="fill:url(#paint147_linear_1690_149725)"/><rect x="59.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="url(#paint148_linear_1690_149725)" style="fill:url(#paint148_linear_1690_149725)"/><rect x="463.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 96 1004.5)" x="96" y="1004.5" width="423.92" height="8" fill="#727171"/><rect transform="rotate(-15 94 1114.2)" x="94" y="1114.2" width="423.92" height="8" fill="#727171"/><path d="m82 1163v-169l32 2v167z" fill="#fff"/><path d="m82 1163v-169l32 2v167z" fill="url(#paint149_linear_1690_149725)" style="fill:url(#paint149_linear_1690_149725)"/><path d="m83.5 1161.5v-165.9l29 1.812v164.09z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m486 1163v-167l32-2v169z" fill="#fff"/><path d="m486 1163v-167l32-2v169z" fill="url(#paint150_linear_1690_149725)" style="fill:url(#paint150_linear_1690_149725)"/><path d="m487.5 1161.5v-164.09l29-1.812v165.9z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 68.875 1013)" x="68.875" y="1013" width="483.9" height="12" fill="#838282"/><rect transform="rotate(-15 63.922 1138.8)" x="63.922" y="1138.8" width="487.78" height="12" fill="#838282"/><circle cx="534" cy="885" r="32" fill="url(#paint151_radial_1690_149725)" style="fill:url(#paint151_radial_1690_149725)"/><circle cx="534" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="66" cy="885" r="32" fill="url(#paint152_radial_1690_149725)" style="fill:url(#paint152_radial_1690_149725)"/><circle cx="66" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="url(#paint153_linear_1690_149725)" style="fill:url(#paint153_linear_1690_149725)"/><path d="m51.5 1169.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint154_linear_1690_149725)" style="fill:url(#paint154_linear_1690_149725)"/><path d="m519.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="url(#paint155_linear_1690_149725)" style="fill:url(#paint155_linear_1690_149725)"/><rect x="17.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="url(#paint156_linear_1690_149725)" style="fill:url(#paint156_linear_1690_149725)"/><rect x="485.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m201.79 0s-201.79 0-201.79 201v985.08c0 7.9536 5.3726 13.921 12 13.921h576c6.627 0 12-5.9676 12-13.921v-985.08c0-201-198.21-201-198.21-201h-101.79zm201.21 243.6c-3.866 0-7 3.7608-7 8.4v901.2c0 4.6392 3.134 8.4 7 8.4h44c3.866 0 7-3.7608 7-8.4v-901.2c0-4.6392-3.134-8.4-7-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs> </g><ellipse cx="300" cy="16" rx="292" ry="16" fill="#5D5C5C"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="url(#paint147_linear_1690_149725)" style="fill:url(#paint147_linear_1690_149725)"/><rect x="59.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="url(#paint148_linear_1690_149725)" style="fill:url(#paint148_linear_1690_149725)"/><rect x="463.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 96 1004.5)" x="96" y="1004.5" width="423.92" height="8" fill="#727171"/><rect transform="rotate(-15 94 1114.2)" x="94" y="1114.2" width="423.92" height="8" fill="#727171"/><path d="m82 1163v-169l32 2v167z" fill="#fff"/><path d="m82 1163v-169l32 2v167z" fill="url(#paint149_linear_1690_149725)" style="fill:url(#paint149_linear_1690_149725)"/><path d="m83.5 1161.5v-165.9l29 1.812v164.09z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m486 1163v-167l32-2v169z" fill="#fff"/><path d="m486 1163v-167l32-2v169z" fill="url(#paint150_linear_1690_149725)" style="fill:url(#paint150_linear_1690_149725)"/><path d="m487.5 1161.5v-164.09l29-1.812v165.9z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 68.875 1013)" x="68.875" y="1013" width="483.9" height="12" fill="#838282"/><rect transform="rotate(-15 63.922 1138.8)" x="63.922" y="1138.8" width="487.78" height="12" fill="#838282"/><circle cx="534" cy="885" r="32" fill="url(#paint151_radial_1690_149725)" style="fill:url(#paint151_radial_1690_149725)"/><circle cx="534" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="66" cy="885" r="32" fill="url(#paint152_radial_1690_149725)" style="fill:url(#paint152_radial_1690_149725)"/><circle cx="66" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="url(#paint153_linear_1690_149725)" style="fill:url(#paint153_linear_1690_149725)"/><path d="m51.5 1169.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint154_linear_1690_149725)" style="fill:url(#paint154_linear_1690_149725)"/><path d="m519.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="url(#paint155_linear_1690_149725)" style="fill:url(#paint155_linear_1690_149725)"/><rect x="17.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="url(#paint156_linear_1690_149725)" style="fill:url(#paint156_linear_1690_149725)"/><rect x="485.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m201.79 0s-201.79 0-201.79 201v985.08c0 7.9536 5.3726 13.921 12 13.921h576c6.627 0 12-5.9676 12-13.921v-985.08c0-201-198.21-201-198.21-201h-101.79zm201.21 243.6c-3.866 0-7 3.7608-7 8.4v901.2c0 4.6392 3.134 8.4 7 8.4h44c3.866 0 7-3.7608 7-8.4v-901.2c0-4.6392-3.134-8.4-7-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_1690_149726" x1="600" x2=".018833" y1="510.96" y2="504.56" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1690_149726" x1="600" x2=".018833" y1="510.96" y2="504.56" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/> <stop stop-color="#020202" stop-opacity=".35" offset="0"/>

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

2
application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg

@ -573,7 +573,7 @@
</mask><g filter="url(#filter0_ii_1694_158298)" tb:tag="value-box"> </mask><g filter="url(#filter0_ii_1694_158298)" tb:tag="value-box">
<path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m381 272c0-6.627 5.373-12 12-12h216c6.627 0 12 5.373 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12v-56z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/> <path d="m382.5 272c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56z" stroke="#727171" stroke-width="3"/>
<text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="499.84497" y="300.37811" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><g transform="matrix(1 0 0 .5 .64 .00025)"> </g><g transform="matrix(1 0 0 .5 .64 .00025)">
<path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/> <path d="m335.68-5e-4s-336.32 0-336.32 201v985.08c0 7.9536 8.9543 13.921 20 13.921h960c11.045 0 20-5.9676 20-13.921v-985.08c0-201-330.35-201-330.35-201h-169.65zm335.35 243.6c-6.4433 0-11.667 3.7608-11.667 8.4v901.2c0 4.6392 5.2233 8.4 11.667 8.4h73.333c6.4433 0 11.667-3.7608 11.667-8.4v-901.2c0-4.6392-5.2233-8.4-11.667-8.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/>
</g><rect x="108" y="763" width="80" height="24" rx="7" fill="#fff"/><rect x="108" y="763" width="80" height="24" rx="7" fill="url(#paint245_linear_1694_158297)" style="fill:url(#paint245_linear_1694_158297)"/><rect x="109.5" y="764.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="812" y="763" width="80" height="24" rx="7" fill="#fff"/><rect x="812" y="763" width="80" height="24" rx="7" fill="url(#paint246_linear_1694_158297)" style="fill:url(#paint246_linear_1694_158297)"/><rect x="813.5" y="764.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 150.46 600.73)" x="150.46" y="600.73" width="710.35" height="8" fill="#727171"/><rect transform="rotate(-10 145.52 726.54)" x="145.52" y="726.53" width="715.97" height="8" fill="#727171"/><path d="m132 763v-167l32 3v164z" fill="#fff"/><path d="m132 763v-167l32 3v164z" fill="url(#paint247_linear_1694_158297)" style="fill:url(#paint247_linear_1694_158297)"/><path d="m133.5 761.5v-163.85l29 2.719v161.13z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m836 763v-164l32-3v167z" fill="#fff"/><path d="m836 763v-164l32-3v167z" fill="url(#paint248_linear_1694_158297)" style="fill:url(#paint248_linear_1694_158297)"/><path d="m837.5 761.5v-161.13l29-2.719v163.85z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 117.91 607.86)" x="117.91" y="607.85" width="781.6" height="12" fill="#838282"/><rect transform="rotate(-10 110.33 744.6)" x="110.33" y="744.6" width="793.89" height="12" fill="#838282"/><circle cx="892" cy="485" r="32" fill="url(#paint249_radial_1694_158297)" style="fill:url(#paint249_radial_1694_158297)"/><circle cx="892" cy="485" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="108" cy="485" r="32" fill="url(#paint250_radial_1694_158297)" style="fill:url(#paint250_radial_1694_158297)"/><circle cx="108" cy="485" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m92 771v-284c0-8.837 7.1634-16 16-16 8.837 0 16 7.163 16 16v284z" fill="#fff"/><path d="m92 771v-284c0-8.837 7.1634-16 16-16 8.837 0 16 7.163 16 16v284z" fill="url(#paint251_linear_1694_158297)" style="fill:url(#paint251_linear_1694_158297)"/><path d="m93.5 769.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5 8.008 0 14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m876 771v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m876 771v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1694_158297)" style="fill:url(#paint252_linear_1694_158297)"/><path d="m877.5 769.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="58" y="771" width="100" height="29" rx="7" fill="#fff"/><rect x="58" y="771" width="100" height="29" rx="7" fill="url(#paint253_linear_1694_158297)" style="fill:url(#paint253_linear_1694_158297)"/><rect x="59.5" y="772.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="842" y="771" width="100" height="29" rx="7" fill="#fff"/><rect x="842" y="771" width="100" height="29" rx="7" fill="url(#paint254_linear_1694_158297)" style="fill:url(#paint254_linear_1694_158297)"/><rect x="843.5" y="772.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs> </g><rect x="108" y="763" width="80" height="24" rx="7" fill="#fff"/><rect x="108" y="763" width="80" height="24" rx="7" fill="url(#paint245_linear_1694_158297)" style="fill:url(#paint245_linear_1694_158297)"/><rect x="109.5" y="764.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="812" y="763" width="80" height="24" rx="7" fill="#fff"/><rect x="812" y="763" width="80" height="24" rx="7" fill="url(#paint246_linear_1694_158297)" style="fill:url(#paint246_linear_1694_158297)"/><rect x="813.5" y="764.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 150.46 600.73)" x="150.46" y="600.73" width="710.35" height="8" fill="#727171"/><rect transform="rotate(-10 145.52 726.54)" x="145.52" y="726.53" width="715.97" height="8" fill="#727171"/><path d="m132 763v-167l32 3v164z" fill="#fff"/><path d="m132 763v-167l32 3v164z" fill="url(#paint247_linear_1694_158297)" style="fill:url(#paint247_linear_1694_158297)"/><path d="m133.5 761.5v-163.85l29 2.719v161.13z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m836 763v-164l32-3v167z" fill="#fff"/><path d="m836 763v-164l32-3v167z" fill="url(#paint248_linear_1694_158297)" style="fill:url(#paint248_linear_1694_158297)"/><path d="m837.5 761.5v-161.13l29-2.719v163.85z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(10 117.91 607.86)" x="117.91" y="607.85" width="781.6" height="12" fill="#838282"/><rect transform="rotate(-10 110.33 744.6)" x="110.33" y="744.6" width="793.89" height="12" fill="#838282"/><circle cx="892" cy="485" r="32" fill="url(#paint249_radial_1694_158297)" style="fill:url(#paint249_radial_1694_158297)"/><circle cx="892" cy="485" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="108" cy="485" r="32" fill="url(#paint250_radial_1694_158297)" style="fill:url(#paint250_radial_1694_158297)"/><circle cx="108" cy="485" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m92 771v-284c0-8.837 7.1634-16 16-16 8.837 0 16 7.163 16 16v284z" fill="#fff"/><path d="m92 771v-284c0-8.837 7.1634-16 16-16 8.837 0 16 7.163 16 16v284z" fill="url(#paint251_linear_1694_158297)" style="fill:url(#paint251_linear_1694_158297)"/><path d="m93.5 769.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5 8.008 0 14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m876 771v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m876 771v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint252_linear_1694_158297)" style="fill:url(#paint252_linear_1694_158297)"/><path d="m877.5 769.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="58" y="771" width="100" height="29" rx="7" fill="#fff"/><rect x="58" y="771" width="100" height="29" rx="7" fill="url(#paint253_linear_1694_158297)" style="fill:url(#paint253_linear_1694_158297)"/><rect x="59.5" y="772.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="842" y="771" width="100" height="29" rx="7" fill="#fff"/><rect x="842" y="771" width="100" height="29" rx="7" fill="url(#paint254_linear_1694_158297)" style="fill:url(#paint254_linear_1694_158297)"/><rect x="843.5" y="772.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs>

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

2
application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg

@ -537,7 +537,7 @@
</mask><path d="m8 125c-3.866 0-7-3.134-7-7s3.134-7 7-7h786c3.866 0 7 3.134 7 7s-3.134 7-7 7h-786z" fill="#D9D9D9"/><path d="m8 114h786v-6h-786v6zm786 8h-786v6h786v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-790 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4772 10 10 10v-6c-2.2092 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-234-inside-2_1693_189770)"/><g filter="url(#filter0_ii_1693_189770)" tb:tag="value-box"> </mask><path d="m8 125c-3.866 0-7-3.134-7-7s3.134-7 7-7h786c3.866 0 7 3.134 7 7s-3.134 7-7 7h-786z" fill="#D9D9D9"/><path d="m8 114h786v-6h-786v6zm786 8h-786v6h786v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-790 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4772 10 10 10v-6c-2.2092 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-234-inside-2_1693_189770)"/><g filter="url(#filter0_ii_1693_189770)" tb:tag="value-box">
<path d="m281 31c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.6274-5.373 12-12 12h-216c-6.627 0-12-5.3726-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m281 31c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.6274-5.373 12-12 12h-216c-6.627 0-12-5.3726-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m282.5 31c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m282.5 31c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="399.12082" y="62.601822" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="399.12082" y="62.601822" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><defs> </g><defs>
<filter id="filter0_ii_1693_189770" x="277" y="15" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_1693_189770" x="277" y="15" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

2
application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg

@ -566,7 +566,7 @@
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><rect x="58" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="url(#paint147_linear_1687_130893)" style="fill:url(#paint147_linear_1687_130893)"/><rect x="59.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="url(#paint148_linear_1687_130893)" style="fill:url(#paint148_linear_1687_130893)"/><rect x="463.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 96 1004.5)" x="96" y="1004.5" width="423.92" height="8" fill="#727171"/><rect transform="rotate(-15 94 1114.2)" x="94" y="1114.2" width="423.92" height="8" fill="#727171"/><path d="m82 1163v-163h32v163z" fill="#fff"/><path d="m82 1163v-163h32v163z" fill="url(#paint149_linear_1687_130893)" style="fill:url(#paint149_linear_1687_130893)"/><path d="m83.5 1161.5v-160h29v160z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m486 1163v-163h32v163z" fill="#fff"/><path d="m486 1163v-163h32v163z" fill="url(#paint150_linear_1687_130893)" style="fill:url(#paint150_linear_1687_130893)"/><path d="m487.5 1161.5v-160h29v160z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 68.875 1013)" x="68.875" y="1013" width="483.9" height="12" fill="#838282"/><rect transform="rotate(-15 63.922 1138.8)" x="63.922" y="1138.8" width="487.78" height="12" fill="#838282"/><circle cx="534" cy="885" r="32" fill="url(#paint151_radial_1687_130893)" style="fill:url(#paint151_radial_1687_130893)"/><circle cx="534" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="66" cy="885" r="32" fill="url(#paint152_radial_1687_130893)" style="fill:url(#paint152_radial_1687_130893)"/><circle cx="66" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="url(#paint153_linear_1687_130893)" style="fill:url(#paint153_linear_1687_130893)"/><path d="m51.5 1169.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint154_linear_1687_130893)" style="fill:url(#paint154_linear_1687_130893)"/><path d="m519.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="url(#paint155_linear_1687_130893)" style="fill:url(#paint155_linear_1687_130893)"/><rect x="17.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="url(#paint156_linear_1687_130893)" style="fill:url(#paint156_linear_1687_130893)"/><rect x="485.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs> </g><rect x="58" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="58" y="1163" width="80" height="24" rx="7" fill="url(#paint147_linear_1687_130893)" style="fill:url(#paint147_linear_1687_130893)"/><rect x="59.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="#fff"/><rect x="462" y="1163" width="80" height="24" rx="7" fill="url(#paint148_linear_1687_130893)" style="fill:url(#paint148_linear_1687_130893)"/><rect x="463.5" y="1164.5" width="77" height="21" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 96 1004.5)" x="96" y="1004.5" width="423.92" height="8" fill="#727171"/><rect transform="rotate(-15 94 1114.2)" x="94" y="1114.2" width="423.92" height="8" fill="#727171"/><path d="m82 1163v-163h32v163z" fill="#fff"/><path d="m82 1163v-163h32v163z" fill="url(#paint149_linear_1687_130893)" style="fill:url(#paint149_linear_1687_130893)"/><path d="m83.5 1161.5v-160h29v160z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m486 1163v-163h32v163z" fill="#fff"/><path d="m486 1163v-163h32v163z" fill="url(#paint150_linear_1687_130893)" style="fill:url(#paint150_linear_1687_130893)"/><path d="m487.5 1161.5v-160h29v160z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect transform="rotate(15 68.875 1013)" x="68.875" y="1013" width="483.9" height="12" fill="#838282"/><rect transform="rotate(-15 63.922 1138.8)" x="63.922" y="1138.8" width="487.78" height="12" fill="#838282"/><circle cx="534" cy="885" r="32" fill="url(#paint151_radial_1687_130893)" style="fill:url(#paint151_radial_1687_130893)"/><circle cx="534" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><circle cx="66" cy="885" r="32" fill="url(#paint152_radial_1687_130893)" style="fill:url(#paint152_radial_1687_130893)"/><circle cx="66" cy="885" r="30.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m50 1171v-284c0-8.837 7.1634-16 16-16s16 7.163 16 16v284z" fill="url(#paint153_linear_1687_130893)" style="fill:url(#paint153_linear_1687_130893)"/><path d="m51.5 1169.5v-282.5c0-8.008 6.4919-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="#fff"/><path d="m518 1171v-284c0-8.837 7.163-16 16-16s16 7.163 16 16v284z" fill="url(#paint154_linear_1687_130893)" style="fill:url(#paint154_linear_1687_130893)"/><path d="m519.5 1169.5v-282.5c0-8.008 6.492-14.5 14.5-14.5s14.5 6.492 14.5 14.5v282.5z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="16" y="1171" width="100" height="29" rx="7" fill="url(#paint155_linear_1687_130893)" style="fill:url(#paint155_linear_1687_130893)"/><rect x="17.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="#fff"/><rect x="484" y="1171" width="100" height="29" rx="7" fill="url(#paint156_linear_1687_130893)" style="fill:url(#paint156_linear_1687_130893)"/><rect x="485.5" y="1172.5" width="97" height="26" rx="5.5" stroke="#000" stroke-opacity=".12" stroke-width="3"/><defs>
<filter id="filter0_ii_1687_130892" x="176" y="46" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_1687_130892" x="176" y="46" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

2
application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg

@ -745,7 +745,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="1" width="598" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="81" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-off-peak"/><rect x="313" y="81" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-night"/><rect x="181" y="237" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-peak"/><text x="171.2998" y="58.286133" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="off-peak-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="432.2998" y="58.734375" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="301.61865" y="214.39307" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="peak-label" xml:space="default"><tspan dominant-baseline="start">T3</tspan></text><text x="170.09232" y="139.47266" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="off-peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="434.09232" y="139.47266" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="302.09232" y="295.47266" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="299.89453" y="371.67578" fill="#000000" font-family="Roboto" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m201.8-2e-4s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6404 12 4.6404h576c6.627 0 12-1.9892 12-4.6404v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="1" y="1" width="598" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="81" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-off-peak"/><rect x="313" y="81" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-night"/><rect x="181" y="237" width="238" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-peak"/><text x="171.2998" y="58.286133" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="off-peak-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="432.2998" y="58.734375" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="301.61865" y="214.39307" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="peak-label" xml:space="default"><tspan dominant-baseline="start">T3</tspan></text><text x="170.09232" y="139.47266" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="off-peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="434.09232" y="139.47266" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="302.09232" y="295.47266" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="peak-rate" xml:space="default"><tspan dominant-baseline="start">000223</tspan></text><text x="299.89453" y="371.67578" fill="#000000" font-family="Roboto, sans-serif" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m201.8-2e-4s-201.8 0-201.8 67v328.36c0 2.6512 5.3727 4.6404 12 4.6404h576c6.627 0 12-1.9892 12-4.6404v-328.36c0-67-198.21-67-198.21-67h-101.79zm201.21 81.2c-3.8661 0-6.9999 1.2536-6.9999 2.8v300.4c0 1.5464 3.1341 2.8 6.9999 2.8h43.998c3.8661 0 6.9999-1.2536 6.9999-2.8v-300.4c0-1.5464-3.1341-2.8-6.9999-2.8z" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

2
application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg

@ -613,7 +613,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="1" y="1" width="398" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="237" width="302" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-night"/><rect x="49" y="81" width="302" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-day"/><text x="199.70117" y="58.076347" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="day-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="199.70117" y="214.37646" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="200.33984" y="139.47266" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="day-rate" xml:space="default"><tspan dominant-baseline="start">000023</tspan></text><text x="200.33984" y="295.47266" fill="#002878" font-family="Roboto" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000023</tspan></text><text x="199.89453" y="371.67578" fill="#000000" font-family="Roboto" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m134.53-2e-4s-134.53 0-134.53 67v328.36c0 2.6512 3.5818 4.6404 8 4.6404h384c4.418 0 8-1.9892 8-4.6404v-328.36c0-67-132.14-67-132.14-67h-67.858zm134.14 81.2c-2.5774 0-4.6666 1.2536-4.6666 2.8v300.4c0 1.5464 2.0894 2.8 4.6666 2.8h29.332c2.5774 0 4.6666-1.2536 4.6666-2.8v-300.4c0-1.5464-2.0894-2.8-4.6666-2.8z" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="1" y="1" width="398" height="398" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect x="49" y="237" width="302" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-night"/><rect x="49" y="81" width="302" height="82" rx="3" fill="#DEDEDE" stroke="#1A1A1A" stroke-width="2" tb:tag="value-box-day"/><text x="199.70117" y="58.076347" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="day-label" xml:space="default"><tspan dominant-baseline="start">T1</tspan></text><text x="199.70117" y="214.37646" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="night-label"><tspan dominant-baseline="start">T2</tspan></text><text x="200.33984" y="139.47266" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="day-rate" xml:space="default"><tspan dominant-baseline="start">000023</tspan></text><text x="200.33984" y="295.47266" fill="#002878" font-family="Roboto, sans-serif" font-size="48px" font-weight="400" text-anchor="middle" tb:tag="night-rate" xml:space="default"><tspan dominant-baseline="start">000023</tspan></text><text x="199.89453" y="371.67578" fill="#000000" font-family="Roboto, sans-serif" font-size="36px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">kWh</tspan></text><path d="m134.53-2e-4s-134.53 0-134.53 67v328.36c0 2.6512 3.5818 4.6404 8 4.6404h384c4.418 0 8-1.9892 8-4.6404v-328.36c0-67-132.14-67-132.14-67h-67.858zm134.14 81.2c-2.5774 0-4.6666 1.2536-4.6666 2.8v300.4c0 1.5464 2.0894 2.8 4.6666 2.8h29.332c2.5774 0 4.6666-1.2536 4.6666-2.8v-300.4c0-1.5464-2.0894-2.8-4.6666-2.8z" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg

@ -364,7 +364,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect x="17" y="1" width="366" height="598" rx="5" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect transform="matrix(1 0 0 -1 382 173)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 0 173)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 382 373)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 382 573)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 0 573)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><circle cx="66" cy="86" r="10" fill="#198038" tb:tag="indicator"/><text x="83.31543" y="89.589844" fill="#000000" font-family="Roboto" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Connected</tspan></text><path d="m134.53 0s-134.53 0-134.53 100.5v492.54c0 3.9768 3.5818 6.9604 8 6.9604h384c4.418 0 8-2.9838 8-6.9604v-492.54c0-100.5-132.14-100.5-132.14-100.5h-67.86zm134.14 121.8c-2.5774 0-4.6666 1.8804-4.6666 4.2v450.6c0 2.3196 2.0894 4.2 4.6666 4.2h29.332c2.5774 0 4.6666-1.8804 4.6666-4.2v-450.6c0-2.3196-2.0894-4.2-4.6666-4.2z" fill-opacity="0" fill="#000" tb:tag="clickArea"/><g transform="translate(0,516)" fill="#d12730" style="display: none;" tb:tag="critical"> <rect x="17" y="1" width="366" height="598" rx="5" fill="#fff" stroke="#1A1A1A" stroke-width="2" tb:tag="background"/><rect transform="matrix(1 0 0 -1 382 173)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 0 173)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 382 373)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 382 573)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><rect transform="matrix(1 0 0 -1 0 573)" x="1" y="-1" width="16" height="148" rx="1" fill="#999" stroke="#1A1A1A" stroke-width="2"/><circle cx="66" cy="86" r="10" fill="#198038" tb:tag="indicator"/><text x="83.31543" y="89.589844" fill="#000000" font-family="Roboto, sans-serif" font-size="30px" font-weight="400" tb:tag="label" xml:space="preserve"><tspan dominant-baseline="middle">Connected</tspan></text><path d="m134.53 0s-134.53 0-134.53 100.5v492.54c0 3.9768 3.5818 6.9604 8 6.9604h384c4.418 0 8-2.9838 8-6.9604v-492.54c0-100.5-132.14-100.5-132.14-100.5h-67.86zm134.14 121.8c-2.5774 0-4.6666 1.8804-4.6666 4.2v450.6c0 2.3196 2.0894 4.2 4.6666 4.2h29.332c2.5774 0 4.6666-1.8804 4.6666-4.2v-450.6c0-2.3196-2.0894-4.2-4.6666-4.2z" fill-opacity="0" fill="#000" tb:tag="clickArea"/><g transform="translate(0,516)" fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg

@ -536,7 +536,7 @@
</mask><path d="m8 125c-3.866 0-7-3.134-7-7s3.134-7 7-7h786c3.866 0 7 3.134 7 7s-3.134 7-7 7h-786z" fill="#D9D9D9"/><path d="m8 114h786v-6h-786v6zm786 8h-786v6h786v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-790 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4772 10 10 10v-6c-2.2092 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-234-inside-2_1693_189770)"/><g filter="url(#filter0_ii_1693_189770)" tb:tag="value-box"> </mask><path d="m8 125c-3.866 0-7-3.134-7-7s3.134-7 7-7h786c3.866 0 7 3.134 7 7s-3.134 7-7 7h-786z" fill="#D9D9D9"/><path d="m8 114h786v-6h-786v6zm786 8h-786v6h786v-6zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10h-6zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10v6zm-790 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10h6zm-6 0c0 5.523 4.4772 10 10 10v-6c-2.2092 0-4-1.791-4-4h-6z" fill="#727171" mask="url(#path-234-inside-2_1693_189770)"/><g filter="url(#filter0_ii_1693_189770)" tb:tag="value-box">
<path d="m281 31c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.6274-5.373 12-12 12h-216c-6.627 0-12-5.3726-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m281 31c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.6274-5.373 12-12 12h-216c-6.627 0-12-5.3726-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m282.5 31c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m282.5 31c0-5.799 4.701-10.5 10.5-10.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="399.12082" y="62.601822" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="399.12082" y="62.601822" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><defs> </g><defs>
<filter id="filter0_ii_1693_189770" x="277" y="15" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_1693_189770" x="277" y="15" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

2
application/src/main/data/json/system/scada_symbols/vertical-tank.svg

@ -566,7 +566,7 @@
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text> <text x="301.5625" y="93.546875" fill="#727171" font-family="Roboto, sans-serif" font-size="32px" font-weight="500" tb:tag="value-text" xml:space="preserve" text-anchor="middle"><tspan dominant-baseline="middle">1660 gal</tspan></text>
</g><defs> </g><defs>
<filter id="filter0_ii_1687_130892" x="176" y="46" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"> <filter id="filter0_ii_1687_130892" x="176" y="46" width="248" height="88" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/> <feFlood flood-opacity="0" result="BackgroundImageFix"/>

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

2
application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg

@ -426,7 +426,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect width="200" height="400" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="198" height="398" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="100" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="52" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="148" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="100" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="52" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="148" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="36" y="137" width="128" height="80" rx="2" fill="#DEDEDE" tb:tag="value-box"/><rect x="37" y="138" width="126" height="78" rx="1" stroke="#000" stroke-opacity=".87" stroke-width="2"/><text x="99.890625" y="190.70906" fill="#002878" font-family="Roboto" font-size="32px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="start">220</tspan></text><text x="99.643097" y="257.79694" fill="black" font-family="Roboto" font-size="28px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">v</tspan></text><path d="m67.265 0s-67.265 0-67.265 67v328.36c0 2.6512 1.7909 4.6404 4 4.6404h192c2.209 0 4-1.9892 4-4.6404v-328.36c0-67-66.07-67-66.07-67h-33.929zm67.07 81.2c-1.2887 0-2.3333 1.2536-2.3333 2.8v300.4c0 1.5464 1.0447 2.8 2.3333 2.8h14.666c1.2887 0 2.3333-1.2536 2.3333-2.8v-300.4c0-1.5464-1.0447-2.8-2.3333-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect width="200" height="400" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="198" height="398" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="100" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="52" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="148" cy="50" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="100" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="52" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><circle cx="148" cy="350" r="19" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="36" y="137" width="128" height="80" rx="2" fill="#DEDEDE" tb:tag="value-box"/><rect x="37" y="138" width="126" height="78" rx="1" stroke="#000" stroke-opacity=".87" stroke-width="2"/><text x="99.890625" y="190.70906" fill="#002878" font-family="Roboto, sans-serif" font-size="32px" font-weight="400" text-anchor="middle" tb:tag="value" xml:space="preserve"><tspan dominant-baseline="start">220</tspan></text><text x="99.643097" y="257.79694" fill="black" font-family="Roboto, sans-serif" font-size="28px" font-weight="400" text-anchor="middle" tb:tag="units" xml:space="preserve"><tspan dominant-baseline="start">v</tspan></text><path d="m67.265 0s-67.265 0-67.265 67v328.36c0 2.6512 1.7909 4.6404 4 4.6404h192c2.209 0 4-1.9892 4-4.6404v-328.36c0-67-66.07-67-66.07-67h-33.929zm67.07 81.2c-1.2887 0-2.3333 1.2536-2.3333 2.8v300.4c0 1.5464 1.0447 2.8 2.3333 2.8h14.666c1.2887 0 2.3333-1.2536 2.3333-2.8v-300.4c0-1.5464-1.0447-2.8-2.3333-2.8z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

2
application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg

@ -570,7 +570,7 @@
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<rect width="400" height="200" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="398" height="198" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="213" y="81" width="126" height="78" rx="1" fill="#dedede" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="out-value-box"/><text x="275.76855" y="136.65625" fill="#002878" font-family="Roboto" font-size="44px" font-weight="400" text-anchor="middle" tb:tag="out-value" xml:space="default"><tspan dominant-baseline="start">220</tspan></text><rect x="61" y="81" width="126" height="78" rx="1" fill="#dedede" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="in-value-box"/><text x="123.76855" y="136.65625" fill="#002878" font-family="Roboto" font-size="44px" font-weight="400" text-anchor="middle" tb:tag="in-value" xml:space="default"><tspan dominant-baseline="start">230</tspan></text><text x="123.29346" y="57.832031" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="in-label" xml:space="default"><tspan dominant-baseline="start">in</tspan></text><text x="275.96484" y="55.873047" fill="#000000" font-family="Roboto" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="out-label" xml:space="default"><tspan dominant-baseline="start">out</tspan></text><path d="m134.53 0s-134.53 0-134.53 33.5v164.18c0 1.3256 3.5818 2.3202 8 2.3202h384c4.418 0 8-0.9946 8-2.3202v-164.18c0-33.5-132.14-33.5-132.14-33.5h-67.858zm134.14 40.6c-2.5774 0-4.6666 0.6268-4.6666 1.4v150.2c0 0.7732 2.0894 1.4 4.6666 1.4h29.332c2.5774 0 4.6666-0.6268 4.6666-1.4v-150.2c0-0.7732-2.0894-1.4-4.6666-1.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical"> <rect width="400" height="200" fill="#fff" tb:tag="background"/><rect x="1" y="1" width="398" height="198" stroke="#000" stroke-opacity=".87" stroke-width="2"/><rect x="213" y="81" width="126" height="78" rx="1" fill="#dedede" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="out-value-box"/><text x="275.76855" y="136.65625" fill="#002878" font-family="Roboto, sans-serif" font-size="44px" font-weight="400" text-anchor="middle" tb:tag="out-value" xml:space="default"><tspan dominant-baseline="start">220</tspan></text><rect x="61" y="81" width="126" height="78" rx="1" fill="#dedede" stroke="#000" stroke-opacity=".87" stroke-width="2" tb:tag="in-value-box"/><text x="123.76855" y="136.65625" fill="#002878" font-family="Roboto, sans-serif" font-size="44px" font-weight="400" text-anchor="middle" tb:tag="in-value" xml:space="default"><tspan dominant-baseline="start">230</tspan></text><text x="123.29346" y="57.832031" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="in-label" xml:space="default"><tspan dominant-baseline="start">in</tspan></text><text x="275.96484" y="55.873047" fill="#000000" font-family="Roboto, sans-serif" font-size="34px" font-weight="400" text-anchor="middle" tb:tag="out-label" xml:space="default"><tspan dominant-baseline="start">out</tspan></text><path d="m134.53 0s-134.53 0-134.53 33.5v164.18c0 1.3256 3.5818 2.3202 8 2.3202h384c4.418 0 8-0.9946 8-2.3202v-164.18c0-33.5-132.14-33.5-132.14-33.5h-67.858zm134.14 40.6c-2.5774 0-4.6666 0.6268-4.6666 1.4v150.2c0 0.7732 2.0894 1.4 4.6666 1.4h29.332c2.5774 0 4.6666-0.6268 4.6666-1.4v-150.2c0-0.7732-2.0894-1.4-4.6666-1.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><g fill="#d12730" style="display: none;" tb:tag="critical">
<rect width="84" height="84" rx="4" fill="#fff" style=""/> <rect width="84" height="84" rx="4" fill="#fff" style=""/>
<rect width="84" height="84" rx="4" style=""/> <rect width="84" height="84" rx="4" style=""/>
<rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/> <rect x="2" y="2" width="80" height="80" rx="2" stroke="#000" stroke-opacity=".87" stroke-width="4" style=""/>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

13
application/src/main/data/upgrade/basic/schema_update.sql

@ -18,8 +18,15 @@
ALTER TABLE ota_package ALTER TABLE ota_package
ADD COLUMN IF NOT EXISTS external_id uuid; ADD COLUMN IF NOT EXISTS external_id uuid;
ALTER TABLE ota_package
ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); DO
$$
BEGIN
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN
ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
END IF;
END;
$$;
-- UPDATE OTA PACKAGE EXTERNAL ID END -- UPDATE OTA PACKAGE EXTERNAL ID END
@ -35,3 +42,5 @@ DROP INDEX IF EXISTS idx_customer_external_id;
DROP INDEX IF EXISTS idx_widgets_bundle_external_id; DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END
ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255);

10
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -35,6 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -62,6 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -311,6 +313,14 @@ public class ActorSystemContext {
@Getter @Getter
private AuditLogService auditLogService; private AuditLogService auditLogService;
@Autowired
@Getter
private RuleEngineAiChatModelService aiChatModelService;
@Autowired
@Getter
private AiModelService aiModelService;
@Autowired @Autowired
@Getter @Getter
private EntityViewService entityViewService; private EntityViewService entityViewService;

11
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -270,10 +270,19 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
rpc.setExpirationTime(request.getExpirationTime()); rpc.setExpirationTime(request.getExpirationTime());
rpc.setRequest(JacksonUtil.valueToTree(request)); rpc.setRequest(JacksonUtil.valueToTree(request));
rpc.setStatus(status); rpc.setStatus(status);
rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo())); rpc.setAdditionalInfo(getAdditionalInfo(request));
systemContext.getTbRpcService().save(tenantId, rpc); systemContext.getTbRpcService().save(tenantId, rpc);
} }
private JsonNode getAdditionalInfo(ToDeviceRpcRequest request) {
try {
return JacksonUtil.toJsonNode(request.getAdditionalInfo());
} catch (IllegalArgumentException e) {
log.debug("Failed to parse additional info [{}]", request.getAdditionalInfo());
return JacksonUtil.valueToTree(request.getAdditionalInfo());
}
}
private ToDeviceRpcRequestMsg createToDeviceRpcRequestMsg(ToDeviceRpcRequest request) { private ToDeviceRpcRequestMsg createToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
ToDeviceRpcRequestBody body = request.getBody(); ToDeviceRpcRequestBody body = request.getBody();
return ToDeviceRpcRequestMsg.newBuilder() return ToDeviceRpcRequestMsg.newBuilder()

12
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -76,6 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -1024,6 +1026,16 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getAuditLogService(); return mainCtx.getAuditLogService();
} }
@Override
public RuleEngineAiChatModelService getAiChatModelService() {
return mainCtx.getAiChatModelService();
}
@Override
public AiModelService getAiModelService() {
return mainCtx.getAiModelService();
}
@Override @Override
public MqttClientSettings getMqttClientSettings() { public MqttClientSettings getMqttClientSettings() {
return mainCtx.getMqttClientSettings(); return mainCtx.getMqttClientSettings();

20
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -26,9 +26,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@ -183,15 +181,12 @@ public class ThingsboardSecurityConfiguration {
} }
@Bean @Bean
public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception { public AuthenticationManager authenticationManager() {
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor return new ProviderManager(List.of(
.postProcess(new DefaultAuthenticationEventPublisher()); restAuthenticationProvider,
var auth = new AuthenticationManagerBuilder(objectPostProcessor); jwtAuthenticationProvider,
auth.authenticationEventPublisher(eventPublisher); refreshTokenAuthenticationProvider
auth.authenticationProvider(restAuthenticationProvider); ));
auth.authenticationProvider(jwtAuthenticationProvider);
auth.authenticationProvider(refreshTokenAuthenticationProvider);
return auth.build();
} }
@Autowired @Autowired
@ -265,4 +260,5 @@ public class ThingsboardSecurityConfiguration {
return new CorsFilter(source); return new CorsFilter(source);
} }
} }
} }

61
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -37,14 +37,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
@ -125,14 +124,13 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) @GetMapping(value = "/settings/{key}")
@ResponseBody
public AdminSettings getAdminSettings( public AdminSettings getAdminSettings(
@Parameter(description = "A string value of the key (e.g. 'general' or 'mail').") @Parameter(description = "A string value of the key (e.g. 'general' or 'mail').")
@PathVariable("key") String key) throws ThingsboardException { @PathVariable("key") String key) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key);
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
} }
@ -144,15 +142,14 @@ public class AdminController extends BaseController {
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
"Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings", method = RequestMethod.POST) @PostMapping(value = "/settings")
@ResponseBody
public AdminSettings saveAdminSettings( public AdminSettings saveAdminSettings(
@Parameter(description = "A JSON value representing the Administration Settings.") @Parameter(description = "A JSON value representing the Administration Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException { @RequestBody AdminSettings adminSettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
adminSettings.setTenantId(getTenantId()); adminSettings.setTenantId(getTenantId());
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
mailService.updateMailConfiguration(); mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
@ -165,8 +162,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the Security Settings object (getSecuritySettings)", @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)",
notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.GET) @GetMapping(value = "/securitySettings")
@ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException { public SecuritySettings getSecuritySettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(securitySettingsService.getSecuritySettings()); return checkNotNull(securitySettingsService.getSecuritySettings());
@ -175,8 +171,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)", @ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.POST) @PostMapping(value = "/securitySettings")
@ResponseBody
public SecuritySettings saveSecuritySettings( public SecuritySettings saveSecuritySettings(
@Parameter(description = "A JSON value representing the Security Settings.") @Parameter(description = "A JSON value representing the Security Settings.")
@RequestBody SecuritySettings securitySettings) throws ThingsboardException { @RequestBody SecuritySettings securitySettings) throws ThingsboardException {
@ -188,8 +183,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) @GetMapping(value = "/jwtSettings")
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException { public JwtSettings getJwtSettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings()); return checkNotNull(jwtSettingsService.getJwtSettings());
@ -198,8 +192,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)", @ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) @PostMapping(value = "/jwtSettings")
@ResponseBody
public JwtPair saveJwtSettings( public JwtPair saveJwtSettings(
@Parameter(description = "A JSON value representing the JWT Settings.") @Parameter(description = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException { @RequestBody JwtSettings jwtSettings) throws ThingsboardException {
@ -213,15 +206,15 @@ public class AdminController extends BaseController {
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) @PostMapping(value = "/settings/testMail")
public void sendTestMail( public void sendTestMail(
@Parameter(description = "A JSON value representing the Mail Settings.") @Parameter(description = "A JSON value representing the Mail Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException { @RequestBody AdminSettings adminSettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings); adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) { if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY));
JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken"); JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken");
if (refreshToken == null) { if (refreshToken == null) {
throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL); throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL);
@ -230,7 +223,7 @@ public class AdminController extends BaseController {
settings.put("refreshToken", refreshToken.asText()); settings.put("refreshToken", refreshToken.asText());
} else { } else {
if (!adminSettings.getJsonValue().has("password")) { if (!adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
} }
} }
@ -251,7 +244,7 @@ public class AdminController extends BaseController {
notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) @PostMapping(value = "/settings/testSms")
public void sendTestSms( public void sendTestSms(
@Parameter(description = "A JSON value representing the Test SMS request.") @Parameter(description = "A JSON value representing the Test SMS request.")
@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
@ -325,7 +318,7 @@ public class AdminController extends BaseController {
notes = "Deletes the repository settings." notes = "Deletes the repository settings."
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) @DeleteMapping(value = "/repositorySettings")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public DeferredResult<Void> deleteRepositorySettings() throws Exception { public DeferredResult<Void> deleteRepositorySettings() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
@ -335,7 +328,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Check repository access (checkRepositoryAccess)", @ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) @PostMapping(value = "/repositorySettings/checkAccess")
public DeferredResult<Void> checkRepositoryAccess( public DeferredResult<Void> checkRepositoryAccess(
@Parameter(description = "A JSON value representing the Repository Settings.") @Parameter(description = "A JSON value representing the Repository Settings.")
@RequestBody RepositorySettings settings) throws Exception { @RequestBody RepositorySettings settings) throws Exception {
@ -376,7 +369,7 @@ public class AdminController extends BaseController {
notes = "Deletes the auto commit settings." notes = "Deletes the auto commit settings."
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) @DeleteMapping(value = "/autoCommitSettings")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteAutoCommitSettings() throws ThingsboardException { public void deleteAutoCommitSettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
@ -387,9 +380,8 @@ public class AdminController extends BaseController {
notes = "Check notifications about new platform releases. " notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/updates", method = RequestMethod.GET) @GetMapping(value = "/updates")
@ResponseBody public UpdateMessage checkUpdates() {
public UpdateMessage checkUpdates() throws ThingsboardException {
return updateService.checkUpdates(); return updateService.checkUpdates();
} }
@ -397,9 +389,8 @@ public class AdminController extends BaseController {
notes = "Get main information about system. " notes = "Get main information about system. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/systemInfo", method = RequestMethod.GET) @GetMapping(value = "/systemInfo")
@ResponseBody public SystemInfo getSystemInfo() {
public SystemInfo getSystemInfo() throws ThingsboardException {
return systemInfoService.getSystemInfo(); return systemInfoService.getSystemInfo();
} }
@ -407,8 +398,7 @@ public class AdminController extends BaseController {
notes = "Get information about enabled/disabled features. " notes = "Get information about enabled/disabled features. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) @GetMapping(value = "/featuresInfo")
@ResponseBody
public FeaturesInfo getFeaturesInfo() { public FeaturesInfo getFeaturesInfo() {
return systemInfoService.getFeaturesInfo(); return systemInfoService.getFeaturesInfo();
} }
@ -417,8 +407,7 @@ public class AdminController extends BaseController {
"double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " +
"further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) @GetMapping(value = "/mail/oauth2/loginProcessingUrl")
@ResponseBody
public String getMailProcessingUrl() throws ThingsboardException { public String getMailProcessingUrl() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return "\"/api/admin/mail/oauth2/code\""; return "\"/api/admin/mail/oauth2/code\"";
@ -427,7 +416,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" +
"provider sends authorization code to specified redirect uri.)") "provider sends authorization code to specified redirect uri.)")
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text")
public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
String state = StringUtils.generateSafeToken(); String state = StringUtils.generateSafeToken();
if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
@ -452,7 +441,7 @@ public class AdminController extends BaseController {
.build() + "\""; .build() + "\"";
} }
@RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"})
public void codeProcessingUrl( public void codeProcessingUrl(
@RequestParam(value = "code") String code, @RequestParam(value = "state") String state, @RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {

178
application/src/main/java/org/thingsboard/server/controller/AiModelController.java

@ -0,0 +1,178 @@
/**
* Copyright © 2016-2025 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.controller;
import com.google.common.util.concurrent.ListenableFuture;
import dev.langchain4j.model.chat.request.ChatRequest;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.dto.TbChatRequest;
import org.thingsboard.server.common.data.ai.dto.TbChatResponse;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ai.AiChatModelService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@Validated
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api/ai/model")
class AiModelController extends BaseController {
private final AiChatModelService aiChatModelService;
@ApiOperation(
value = "Create or update AI model (saveAiModel)",
notes = "Creates or updates an AI model record.\n\n" +
"• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new record and returns it in the `id` field of the response.\n\n" +
"• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" +
"Tenant ID for the AI model will be taken from the authenticated user making the request, regardless of any value provided in the request body." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping
public AiModel saveAiModel(@RequestBody @Valid AiModel model) throws ThingsboardException {
var user = getCurrentUser();
model.setTenantId(user.getTenantId());
checkEntity(model.getId(), model, Resource.AI_MODEL);
return tbAiModelService.save(model, user);
}
@ApiOperation(
value = "Get AI model by ID (getAiModelById)",
notes = "Fetches an AI model record by its `id`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/{modelUuid}")
public AiModel getAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
return checkAiModelId(new AiModelId(modelUuid), Operation.READ);
}
@ApiOperation(
value = "Get AI models (getAiModels)",
notes = "Returns a page of AI models. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping
public PageData<AiModel> getAiModels(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = AI_MODEL_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder
) throws ThingsboardException {
var user = getCurrentUser();
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.READ);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiModelService.findAiModelsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(
value = "Delete AI model by ID (deleteAiModelById)",
notes = "Deletes the AI model record by its `id`. " +
"If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " +
"If no such record exists, the endpoint returns `false`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/{modelUuid}")
public boolean deleteAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
var user = getCurrentUser();
var modelId = new AiModelId(modelUuid);
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE);
Optional<AiModel> toDelete = aiModelService.findAiModelByTenantIdAndId(user.getTenantId(), modelId);
if (toDelete.isEmpty()) {
return false;
}
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE, modelId, toDelete.get());
return tbAiModelService.delete(toDelete.get(), user);
}
@ApiOperation(
value = "Send request to AI chat model (sendChatRequest)",
notes = "Submits a single prompt - made up of an optional system message and a required user message - to the specified AI chat model " +
"and returns either the generated answer or an error envelope." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest();
AiChatModelConfig<?> chatModelConfig = tbChatRequest.chatModelConfig();
ListenableFuture<TbChatResponse> future = aiChatModelService.sendChatRequestAsync(chatModelConfig, langChainChatRequest)
.transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor())
.catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor());
Integer requestTimeoutSeconds = chatModelConfig.timeoutSeconds();
return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future);
}
}

16
application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java

@ -19,12 +19,13 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
@ -54,6 +55,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api") @RequestMapping("/api")
public class AlarmCommentController extends BaseController { public class AlarmCommentController extends BaseController {
public static final String ALARM_ID = "alarmId"; public static final String ALARM_ID = "alarmId";
public static final String ALARM_COMMENT_ID = "commentId"; public static final String ALARM_COMMENT_ID = "commentId";
@ -68,8 +70,7 @@ public class AlarmCommentController extends BaseController {
"\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -82,8 +83,7 @@ public class AlarmCommentController extends BaseController {
@ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)",
notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/comment/{commentId}")
@ResponseBody
public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -98,8 +98,7 @@ public class AlarmCommentController extends BaseController {
notes = "Returns a page of alarm comments for specified alarm. " + notes = "Returns a page of alarm comments for specified alarm. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public PageData<AlarmCommentInfo> getAlarmComments( public PageData<AlarmCommentInfo> getAlarmComments(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -118,4 +117,5 @@ public class AlarmCommentController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink));
} }
} }

60
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -21,12 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
@ -58,7 +59,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
@ -104,8 +104,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm (getAlarmById)", @ApiOperation(value = "Get Alarm (getAlarmById)",
notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK) notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}")
@ResponseBody
public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -117,8 +116,7 @@ public class AlarmController extends BaseController {
notes = "Fetch the Alarm Info object based on the provided Alarm Id. " + notes = "Fetch the Alarm Info object based on the provided Alarm Id. " +
ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/info/{alarmId}")
@ResponseBody
public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -136,11 +134,9 @@ public class AlarmController extends BaseController {
"If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " + "If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " +
"If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " + "If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm", method = RequestMethod.POST) @PostMapping(value = "/alarm")
@ResponseBody
public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
alarm.setTenantId(getTenantId()); alarm.setTenantId(getTenantId());
checkNotNull(alarm.getOriginator()); checkNotNull(alarm.getOriginator());
@ -155,8 +151,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Delete Alarm (deleteAlarm)", @ApiOperation(value = "Delete Alarm (deleteAlarm)",
notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}")
@ResponseBody
public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -169,7 +164,7 @@ public class AlarmController extends BaseController {
"Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " + "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/ack")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -184,7 +179,7 @@ public class AlarmController extends BaseController {
"Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " + "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/clear")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -200,7 +195,7 @@ public class AlarmController extends BaseController {
"(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/assign/{assigneeId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -221,7 +216,7 @@ public class AlarmController extends BaseController {
"Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/assign")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId @PathVariable(ALARM_ID) String strAlarmId
@ -236,8 +231,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarms( public PageData<AlarmInfo> getAlarms(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -265,7 +259,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -292,8 +286,7 @@ public class AlarmController extends BaseController {
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarms", method = RequestMethod.GET) @GetMapping(value = "/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarms( public PageData<AlarmInfo> getAllAlarms(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ -317,7 +310,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) { if (alarmSearchStatus != null && alarmStatus != null) {
@ -341,8 +334,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. " + notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/v2/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2( public PageData<AlarmInfo> getAlarmsV2(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -370,7 +362,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -407,8 +399,7 @@ public class AlarmController extends BaseController {
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) @GetMapping(value = "/v2/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2( public PageData<AlarmInfo> getAllAlarmsV2(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})))
@RequestParam(required = false) String[] statusList, @RequestParam(required = false) String[] statusList,
@ -432,7 +423,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>(); List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) { if (statusList != null) {
for (String strStatus : statusList) { for (String strStatus : statusList) {
@ -465,11 +456,9 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}")
@ResponseBody
public AlarmSeverity getHighestAlarmSeverity( public AlarmSeverity getHighestAlarmSeverity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -499,8 +488,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm Types (getAlarmTypes)", @ApiOperation(value = "Get Alarm Types (getAlarmTypes)",
notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.") notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/types", method = RequestMethod.GET) @GetMapping(value = "/alarm/types")
@ResponseBody
public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -508,7 +496,7 @@ public class AlarmController extends BaseController {
@Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) @Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder);
return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink));
} }

22
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -37,6 +37,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -75,6 +77,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -129,6 +132,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -175,6 +179,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.entitiy.ai.TbAiModelService;
import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService;
import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -378,6 +383,12 @@ public abstract class BaseController {
@Autowired @Autowired
protected CalculatedFieldService calculatedFieldService; protected CalculatedFieldService calculatedFieldService;
@Autowired
protected AiModelService aiModelService;
@Autowired
protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}") @Value("${server.log_controller_error_stack_trace}")
@Getter @Getter
private boolean logControllerErrorStackTrace; private boolean logControllerErrorStackTrace;
@ -390,7 +401,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) { public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e); ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause(); e = (Exception) thingsboardException.getCause();
} else { } else {
e = thingsboardException; e = thingsboardException;
@ -438,7 +449,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
@ -448,6 +459,8 @@ public abstract class BaseController {
return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE); return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE);
} else if (exception instanceof EntityVersionMismatchException) { } else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
} else if (exception instanceof MethodArgumentTypeMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
} }
@ -634,6 +647,7 @@ public abstract class BaseController {
case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation);
case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation);
default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}; };
} catch (Exception e) { } catch (Exception e) {
@ -837,6 +851,10 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation); return checkEntityId(jobId, jobService::findJobById, operation);
} }
AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation);
}
protected <I extends EntityId> I emptyId(EntityType entityType) { protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
} }

109
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -17,19 +17,21 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -41,7 +43,6 @@ import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.EventType;
@ -49,17 +50,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldScriptEngine;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList; import java.util.ArrayList;
@ -100,30 +98,30 @@ public class CalculatedFieldController extends BaseController {
private static final String TEST_SCRIPT_EXPRESSION = private static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n" "Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ "{\n" + + "{\n" +
" \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" +
" \"arguments\": {\n" + " \"arguments\": {\n" +
" \"temperature\": {\n" + " \"temperature\": {\n" +
" \"type\": \"TS_ROLLING\",\n" + " \"type\": \"TS_ROLLING\",\n" +
" \"timeWindow\": {\n" + " \"timeWindow\": {\n" +
" \"startTs\": 1739775630002,\n" + " \"startTs\": 1739775630002,\n" +
" \"endTs\": 65432211,\n" + " \"endTs\": 65432211,\n" +
" \"limit\": 5\n" + " \"limit\": 5\n" +
" },\n" + " },\n" +
" \"values\": [\n" + " \"values\": [\n" +
" { \"ts\": 1739775639851, \"value\": 23 },\n" + " { \"ts\": 1739775639851, \"value\": 23 },\n" +
" { \"ts\": 1739775664561, \"value\": 43 },\n" + " { \"ts\": 1739775664561, \"value\": 43 },\n" +
" { \"ts\": 1739775713079, \"value\": 15 },\n" + " { \"ts\": 1739775713079, \"value\": 15 },\n" +
" { \"ts\": 1739775999522, \"value\": 34 },\n" + " { \"ts\": 1739775999522, \"value\": 34 },\n" +
" { \"ts\": 1739776228452, \"value\": 22 }\n" + " { \"ts\": 1739776228452, \"value\": 22 }\n" +
" ]\n" + " ]\n" +
" },\n" + " },\n" +
" \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" +
" }\n" + " }\n" +
"}" "}"
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n Expected result JSON contains \"output\" and \"error\"."; + "\n\n Expected result JSON contains \"output\" and \"error\".";
@ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)",
notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK +
@ -133,13 +131,12 @@ public class CalculatedFieldController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. " "Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. "
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField", method = RequestMethod.POST) @PostMapping("/calculatedField")
@ResponseBody
public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.") public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.")
@RequestBody CalculatedField calculatedField) throws Exception { @RequestBody CalculatedField calculatedField) throws Exception {
calculatedField.setTenantId(getTenantId()); calculatedField.setTenantId(getTenantId());
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser()); checkReferencedEntities(calculatedField.getConfiguration());
return tbCalculatedFieldService.save(calculatedField, getCurrentUser()); return tbCalculatedFieldService.save(calculatedField, getCurrentUser());
} }
@ -147,8 +144,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Fetch the Calculated Field object based on the provided Calculated Field Id." notes = "Fetch the Calculated Field object based on the provided Calculated Field Id."
) )
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.GET) @GetMapping("/calculatedField/{calculatedFieldId}")
@ResponseBody
public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -162,8 +158,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Fetch the Calculated Fields based on the provided Entity Id." notes = "Fetch the Calculated Fields based on the provided Entity Id."
) )
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"})
@ResponseBody
public PageData<CalculatedField> getCalculatedFieldsByEntityId( public PageData<CalculatedField> getCalculatedFieldsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -182,8 +177,8 @@ public class CalculatedFieldController extends BaseController {
@ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)",
notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE) @DeleteMapping("/calculatedField/{calculatedFieldId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -196,8 +191,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Gets latest calculated field debug event for specified calculated field id. " + notes = "Gets latest calculated field debug event for specified calculated field id. " +
"Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) "Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}/debug", method = RequestMethod.GET) @GetMapping("/calculatedField/{calculatedFieldId}/debug")
@ResponseBody
public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -212,15 +206,13 @@ public class CalculatedFieldController extends BaseController {
@ApiOperation(value = "Test Script expression", @ApiOperation(value = "Test Script expression",
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/testScript", method = RequestMethod.POST) @PostMapping("/calculatedField/testScript")
@ResponseBody
public JsonNode testScript( public JsonNode testScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) { @RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText(); String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse( Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
}),
Collections.emptyMap() Collections.emptyMap()
); );
@ -231,12 +223,13 @@ public class CalculatedFieldController extends BaseController {
String output = ""; String output = "";
String errorText = ""; String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try { try {
if (tbelInvokeService == null) { if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!"); throw new IllegalArgumentException("TBEL script engine is disabled!");
} }
CalculatedFieldScriptEngine calculatedFieldScriptEngine = new CalculatedFieldTbelScriptEngine( engine = new CalculatedFieldTbelScriptEngine(
getTenantId(), getTenantId(),
tbelInvokeService, tbelInvokeService,
expression, expression,
@ -254,17 +247,20 @@ public class CalculatedFieldController extends BaseController {
} }
} }
JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json); output = JacksonUtil.toString(json);
} catch (Exception e) { } catch (Exception e) {
log.error("Error evaluating expression", e); log.error("Error evaluating expression", e);
errorText = e.getMessage(); Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
} }
return JacksonUtil.newObjectNode()
ObjectNode result = JacksonUtil.newObjectNode(); .put("output", output)
result.put("output", output); .put("error", errorText);
result.put("error", errorText);
return result;
} }
private long getLatestTimestamp(Map<String, TbelCfArg> arguments) { private long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
@ -281,7 +277,7 @@ public class CalculatedFieldController extends BaseController {
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
} }
private <E extends HasId<I> & HasTenantId, I extends EntityId> void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig, SecurityUser user) throws ThingsboardException { private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) { for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType(); EntityType entityType = referencedEntityId.getEntityType();
@ -290,8 +286,7 @@ public class CalculatedFieldController extends BaseController {
return; return;
} }
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
} }
} }

3
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -31,7 +31,7 @@ public class ControllerConstants {
protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String ASSIGNEE_ID = "assigneeId";
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. "; "See response schema for more details. ";
protected static final String INLINE_IMAGES = "inlineImages"; protected static final String INLINE_IMAGES = "inlineImages";
protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)";
@ -90,6 +90,7 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";

15
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
@ -76,6 +77,7 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.") @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.")
@RequestBody EntityCountQuery query) throws ThingsboardException { @RequestBody EntityCountQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
} }
@ -87,6 +89,7 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@RequestBody EntityDataQuery query) throws ThingsboardException { @RequestBody EntityDataQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
} }
@ -103,6 +106,7 @@ public class EntityQueryController extends BaseController {
if (assigneeId != null) { if (assigneeId != null) {
checkUserId(assigneeId, Operation.READ); checkUserId(assigneeId, Operation.READ);
} }
resolveQuery(query);
return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
} }
@ -117,6 +121,7 @@ public class EntityQueryController extends BaseController {
if (assigneeId != null) { if (assigneeId != null) {
checkUserId(assigneeId, Operation.READ); checkUserId(assigneeId, Operation.READ);
} }
resolveQuery(query);
return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
} }
@ -136,6 +141,7 @@ public class EntityQueryController extends BaseController {
@RequestParam(value = "scope", required = false) String scope) throws ThingsboardException { @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException {
TenantId tenantId = getTenantId(); TenantId tenantId = getTenantId();
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink(); EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) { if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE); pageLink.setPageSize(MAX_PAGE_SIZE);
@ -155,4 +161,13 @@ public class EntityQueryController extends BaseController {
return edqsService.getState(); return edqsService.getState();
} }
private void resolveQuery(EntityCountQuery query) throws ThingsboardException {
if (query.getEntityFilter() != null) {
var user = getCurrentUser();
var customerId = user.getCustomerId();
var ownerId = customerId != null && !customerId.isNullUid() ? customerId : getTenantId();
EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), user.getId(), ownerId);
}
}
} }

163
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
@ -23,16 +22,19 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -155,9 +157,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain getRuleChainById( public RuleChain getRuleChainById(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -169,9 +170,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)",
notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. " notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. "
+ RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/output/labels")
@ResponseBody
public Set<String> getRuleChainOutputLabels( public Set<String> getRuleChainOutputLabels(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -184,9 +184,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)",
notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. " notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. "
+ RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels/usage", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/output/labels/usage")
@ResponseBody
public List<RuleChainOutputLabelsUsage> getRuleChainOutputLabelsUsage( public List<RuleChainOutputLabelsUsage> getRuleChainOutputLabelsUsage(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -198,9 +197,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/metadata")
@ResponseBody
public RuleChainMetaData getRuleChainMetaData( public RuleChainMetaData getRuleChainMetaData(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -218,9 +216,8 @@ public class RuleChainController extends BaseController {
"\n\n" + RULE_CHAIN_DESCRIPTION + "\n\n" + RULE_CHAIN_DESCRIPTION +
"Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." + "Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain", method = RequestMethod.POST) @PostMapping("/ruleChain")
@ResponseBody
public RuleChain saveRuleChain( public RuleChain saveRuleChain(
@Parameter(description = "A JSON value representing the rule chain.") @Parameter(description = "A JSON value representing the rule chain.")
@RequestBody RuleChain ruleChain) throws Exception { @RequestBody RuleChain ruleChain) throws Exception {
@ -232,9 +229,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Create Default Rule Chain", @ApiOperation(value = "Create Default Rule Chain",
notes = "Create rule chain from template, based on the specified name in the request. " + notes = "Create rule chain from template, based on the specified name in the request. " +
"Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) @PostMapping("/ruleChain/device/default")
@ResponseBody
public RuleChain saveRuleChain( public RuleChain saveRuleChain(
@Parameter(description = "A JSON value representing the request.") @Parameter(description = "A JSON value representing the request.")
@RequestBody DefaultRuleChainCreateRequest request) throws Exception { @RequestBody DefaultRuleChainCreateRequest request) throws Exception {
@ -245,9 +241,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)", @ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)",
notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/root")
@ResponseBody
public RuleChain setRootRuleChain( public RuleChain setRootRuleChain(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -259,9 +254,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Update Rule Chain Metadata", @ApiOperation(value = "Update Rule Chain Metadata",
notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST) @PostMapping("/ruleChain/metadata")
@ResponseBody
public RuleChainMetaData saveRuleChainMetaData( public RuleChainMetaData saveRuleChainMetaData(
@Parameter(description = "A JSON value representing the rule chain metadata.") @Parameter(description = "A JSON value representing the rule chain metadata.")
@RequestBody RuleChainMetaData ruleChainMetaData, @RequestBody RuleChainMetaData ruleChainMetaData,
@ -284,8 +278,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chains (getRuleChains)", @ApiOperation(value = "Get Rule Chains (getRuleChains)",
notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/ruleChains", params = {"pageSize", "page"})
@ResponseBody
public PageData<RuleChain> getRuleChains( public PageData<RuleChain> getRuleChains(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -302,7 +295,7 @@ public class RuleChainController extends BaseController {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
RuleChainType type = RuleChainType.CORE; RuleChainType type = RuleChainType.CORE;
if (typeStr != null && typeStr.trim().length() > 0) { if (StringUtils.isNotBlank(typeStr)) {
type = RuleChainType.valueOf(typeStr); type = RuleChainType.valueOf(typeStr);
} }
return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink));
@ -311,9 +304,9 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Delete rule chain (deleteRuleChain)", @ApiOperation(value = "Delete rule chain (deleteRuleChain)",
notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " + notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " +
"Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH) "Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @DeleteMapping("/ruleChain/{ruleChainId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public void deleteRuleChain( public void deleteRuleChain(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -326,9 +319,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)", @ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)",
notes = "Gets the input message from the debug events for specified Rule Chain Id. " + notes = "Gets the input message from the debug events for specified Rule Chain Id. " +
"Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) "Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) @GetMapping("/ruleNode/{ruleNodeId}/debugIn")
@ResponseBody
public JsonNode getLatestRuleNodeDebugInput( public JsonNode getLatestRuleNodeDebugInput(
@Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException {
@ -343,8 +335,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Is TBEL script executor enabled", @ApiOperation(value = "Is TBEL script executor enabled",
notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET) @GetMapping("/ruleChain/tbelEnabled")
@ResponseBody
public Boolean isTbelEnabled() { public Boolean isTbelEnabled() {
return tbelEnabled; return tbelEnabled;
} }
@ -352,13 +343,12 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Test Script function", @ApiOperation(value = "Test Script function",
notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @PostMapping("/ruleChain/testScript")
@ResponseBody
public JsonNode testScript( public JsonNode testScript(
@Parameter(description = "Script language: JS or TBEL") @Parameter(description = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang, @RequestParam(required = false) ScriptLanguage scriptLang,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException, JsonProcessingException { @RequestBody JsonNode inputParams) {
String script = inputParams.get("script").asText(); String script = inputParams.get("script").asText();
String scriptType = inputParams.get("scriptType").asText(); String scriptType = inputParams.get("scriptType").asText();
JsonNode argNamesJson = inputParams.get("argNames"); JsonNode argNamesJson = inputParams.get("argNames");
@ -366,8 +356,7 @@ public class RuleChainController extends BaseController {
String data = inputParams.get("msg").asText(); String data = inputParams.get("msg").asText();
JsonNode metadataJson = inputParams.get("metadata"); JsonNode metadataJson = inputParams.get("metadata");
Map<String, String> metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<Map<String, String>>() { Map<String, String> metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<>() {});
});
String msgType = inputParams.get("msgType").asText(); String msgType = inputParams.get("msgType").asText();
String output = ""; String output = "";
String errorText = ""; String errorText = "";
@ -384,55 +373,40 @@ public class RuleChainController extends BaseController {
} }
engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames);
} }
TbMsg inMsg = TbMsg.newMsg()
var inMsg = TbMsg.newMsg()
.type(msgType) .type(msgType)
.copyMetaData(new TbMsgMetaData(metadata)) .copyMetaData(new TbMsgMetaData(metadata))
.dataType(TbMsgDataType.JSON) .dataType(TbMsgDataType.JSON)
.data(data) .data(data)
.build(); .build();
switch (scriptType) {
case "update": output = switch (scriptType) {
output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); case "update" -> msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; case "generate" -> msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
case "generate": case "filter" -> Boolean.toString(engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); case "switch" -> JacksonUtil.toString(engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; case "json" -> JacksonUtil.toString(engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
case "filter": case "string" -> engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); default -> throw new IllegalArgumentException("Unsupported script type: " + scriptType);
output = Boolean.toString(result); };
break;
case "switch":
Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(states);
break;
case "json":
JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
break;
case "string":
output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
break;
default:
throw new IllegalArgumentException("Unsupported script type: " + scriptType);
}
} catch (Exception e) { } catch (Exception e) {
log.error("Error evaluating JS function", e); log.error("Error evaluating JS function", e);
errorText = e.getMessage(); Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally { } finally {
if (engine != null) { if (engine != null) {
engine.destroy(); engine.destroy();
} }
} }
ObjectNode result = JacksonUtil.newObjectNode(); return JacksonUtil.newObjectNode()
result.put("output", output); .put("output", output)
result.put("error", errorText); .put("error", errorText);
return result;
} }
@ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) @GetMapping(value = "/ruleChains/export", params = {"limit"})
@ResponseBody
public RuleChainData exportRuleChains( public RuleChainData exportRuleChains(
@Parameter(description = "A limit of rule chains to export.", required = true) @Parameter(description = "A limit of rule chains to export.", required = true)
@RequestParam("limit") int limit) throws ThingsboardException { @RequestParam("limit") int limit) throws ThingsboardException {
@ -443,8 +417,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) @PostMapping("/ruleChains/import")
@ResponseBody
public List<RuleChainImportResult> importRuleChains( public List<RuleChainImportResult> importRuleChains(
@Parameter(description = "A JSON value representing the rule chains.") @Parameter(description = "A JSON value representing the rule chains.")
@RequestBody RuleChainData ruleChainData, @RequestBody RuleChainData ruleChainData,
@ -454,12 +427,12 @@ public class RuleChainController extends BaseController {
return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration); return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration);
} }
private String msgToOutput(TbMsg msg) throws Exception { private String msgToOutput(TbMsg msg) {
JsonNode resultNode = convertMsgToOut(msg); JsonNode resultNode = convertMsgToOut(msg);
return JacksonUtil.toString(resultNode); return JacksonUtil.toString(resultNode);
} }
private String msgToOutput(List<TbMsg> msgs) throws Exception { private String msgToOutput(List<TbMsg> msgs) {
JsonNode resultNode; JsonNode resultNode;
if (msgs.size() > 1) { if (msgs.size() > 1) {
resultNode = JacksonUtil.newArrayNode(); resultNode = JacksonUtil.newArrayNode();
@ -473,7 +446,7 @@ public class RuleChainController extends BaseController {
return JacksonUtil.toString(resultNode); return JacksonUtil.toString(resultNode);
} }
private JsonNode convertMsgToOut(TbMsg msg) throws Exception { private JsonNode convertMsgToOut(TbMsg msg) {
ObjectNode msgData = JacksonUtil.newObjectNode(); ObjectNode msgData = JacksonUtil.newObjectNode();
if (!StringUtils.isEmpty(msg.getData())) { if (!StringUtils.isEmpty(msg.getData())) {
msgData.set("msg", JacksonUtil.toJsonNode(msg.getData())); msgData.set("msg", JacksonUtil.toJsonNode(msg.getData()));
@ -492,8 +465,7 @@ public class RuleChainController extends BaseController {
"Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " + "Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " +
"\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH) "\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) @PostMapping("/edge/{edgeId}/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
@ -514,8 +486,7 @@ public class RuleChainController extends BaseController {
EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH) "Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @DeleteMapping("/edge/{edgeId}/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
@ -530,9 +501,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)",
notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"})
@ResponseBody
public PageData<RuleChain> getEdgeRuleChains( public PageData<RuleChain> getEdgeRuleChains(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@ -557,9 +527,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)",
notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " + notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " +
"Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/edgeTemplateRoot")
@ResponseBody
public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -572,8 +541,7 @@ public class RuleChainController extends BaseController {
notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " + notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " +
"Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/autoAssignToEdge")
@ResponseBody
public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -586,8 +554,7 @@ public class RuleChainController extends BaseController {
notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " + notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " +
"Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) @DeleteMapping("/ruleChain/{ruleChainId}/autoAssignToEdge")
@ResponseBody
public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -599,9 +566,8 @@ public class RuleChainController extends BaseController {
// TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign // TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign
@ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)", @ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)",
notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) @GetMapping("/ruleChain/autoAssignToEdgeRuleChains")
@ResponseBody
public List<RuleChain> getAutoAssignToEdgeRuleChains() throws ThingsboardException { public List<RuleChain> getAutoAssignToEdgeRuleChains() throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChain> result = new ArrayList<>(); List<RuleChain> result = new ArrayList<>();
@ -612,4 +578,5 @@ public class RuleChainController extends BaseController {
} }
return checkNotNull(result); return checkNotNull(result);
} }
} }

20
application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2025 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.ai;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
public interface AiChatModelService extends RuleEngineAiChatModelService {}

40
application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2025 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
@Service
@RequiredArgsConstructor
class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor;
@Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
}
}

27
application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2025 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
public interface AiRequestsExecutor {
FluentFuture<ChatResponse> sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest);
}

86
application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java

@ -0,0 +1,86 @@
/**
* Copyright © 2016-2025 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import java.time.Duration;
import java.util.concurrent.Executors;
@Lazy
@Component
@RequiredArgsConstructor
class DefaultAiRequestsExecutor implements AiRequestsExecutor {
private final AiRequestsExecutorProperties properties;
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool")
private static class AiRequestsExecutorProperties {
@NotBlank(message = "Pool name must be not blank")
private String poolName = "ai-requests";
@Min(value = 1, message = "Pool size must be at least 1")
private int poolSize = 50;
@Min(value = 1, message = "Termination timeout must be at least 1 second")
private int terminationTimeoutSeconds = 60;
}
private ListeningExecutorService executorService;
@PostConstruct
private void init() {
executorService = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(properties.getPoolSize(), ThingsBoardThreadFactory.forName(properties.getPoolName()))
);
}
@Override
public FluentFuture<ChatResponse> sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest) {
return FluentFuture.from(executorService.submit(() -> chatModel.chat(chatRequest)));
}
@PreDestroy
private void destroy() {
if (executorService != null) {
MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(properties.getTerminationTimeoutSeconds()));
executorService = null;
}
}
}

269
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -0,0 +1,269 @@
/**
* Copyright © 2016-2025 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.ai;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
import com.google.cloud.vertexai.api.GenerationConfig;
import com.google.cloud.vertexai.api.PredictionServiceClient;
import com.google.cloud.vertexai.api.PredictionServiceSettings;
import com.google.cloud.vertexai.generativeai.GenerativeModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.Duration;
@Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
return AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey())
.deploymentName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
return GoogleAiGeminiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
// construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials;
try {
serviceAccountCredentials = ServiceAccountCredentials.fromStream(new ByteArrayInputStream(providerConfig.serviceAccountKey().getBytes()));
} catch (IOException e) {
throw new RuntimeException("Failed to parse service account key JSON", e);
}
PredictionServiceSettings predictionServiceClientSettings;
try {
// create prediction service settings for REST transport with service account key credentials
PredictionServiceSettings.Builder settingsBuilder = PredictionServiceSettings.newHttpJsonBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials));
// get the retry settings that control request timeout for generateContent RPC
RetrySettings.Builder retrySettings = settingsBuilder
.generateContentSettings()
.getRetrySettings()
.toBuilder();
// set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
settingsBuilder.generateContentSettings().setRetrySettings(retrySettings.build());
// build the client settings
predictionServiceClientSettings = settingsBuilder.build();
} catch (IOException e) {
throw new RuntimeException("Failed to create prediction service client settings", e);
}
// construct Vertex AI instance
var vertexAI = new VertexAI.Builder()
.setProjectId(providerConfig.projectId())
.setLocation(providerConfig.location())
.setPredictionClientSupplier(() -> createPredictionServiceClient(predictionServiceClientSettings))
.setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys
.build();
// map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder();
if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
}
if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
}
if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(chatModelConfig.topK());
}
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
}
var generationConfig = generationConfigBuilder.build();
// construct generative model instance
var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig);
return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries());
}
private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) {
try {
return PredictionServiceClient.create(settings);
} catch (IOException e) {
throw new RuntimeException("Failed to create prediction service client", e);
}
}
@Override
public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) {
return MistralAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) {
return AnthropicChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
);
var bedrockClient = BedrockRuntimeClient.builder()
.region(Region.of(providerConfig.region()))
.credentialsProvider(credentialsProvider)
.build();
var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.build();
return BedrockChatModel.builder()
.client(bedrockClient)
.modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
return GitHubModelsChatModel.builder()
.gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
}
}

5
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -38,6 +38,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTargetService;
@ -78,6 +79,7 @@ import org.thingsboard.server.service.executors.GrpcCallbackExecutorService;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
@Lazy @Lazy
@Data @Data
@ -198,6 +200,9 @@ public class EdgeContextComponent {
@Autowired @Autowired
private WidgetsBundleService widgetsBundleService; private WidgetsBundleService widgetsBundleService;
@Autowired
private Optional<EdgeStatsCounterService> statsCounterService;
// processors // processors
@Autowired @Autowired

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -113,7 +113,7 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { try {
if (EntityType.TENANT.equals(entityType) || EntityType.EDGE.equals(entityType)) { if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
return; return;
} }
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -227,7 +227,7 @@ public class EdgeEventSourcingListener {
break; break;
case TENANT: case TENANT:
return !event.getCreated(); return !event.getCreated();
case API_USAGE_STATE, EDGE: case API_USAGE_STATE, EDGE, AI_MODEL:
return false; return false;
case DOMAIN: case DOMAIN:
if (entity instanceof Domain domain) { if (entity instanceof Domain domain) {

8
application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java

@ -58,8 +58,7 @@ public class RelatedEdgesSourcingListener {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try { try {
switch (event.getActionType()) { switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
@ -67,7 +66,10 @@ public class RelatedEdgesSourcingListener {
}); });
} }
@TransactionalEventListener(fallbackExecution = true) @TransactionalEventListener(
fallbackExecution = true,
condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL"
)
public void handleEvent(DeleteEntityEvent<?> event) { public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -59,8 +59,7 @@ import org.thingsboard.server.gen.edge.v1.RequestMsg;
import org.thingsboard.server.gen.edge.v1.ResponseMsg; import org.thingsboard.server.gen.edge.v1.ResponseMsg;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.kafka.TbKafkaSettings; import org.thingsboard.server.queue.kafka.KafkaAdmin;
import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -153,10 +152,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private TbCoreQueueFactory tbCoreQueueFactory; private TbCoreQueueFactory tbCoreQueueFactory;
@Autowired @Autowired
private Optional<TbKafkaSettings> kafkaSettings; private Optional<KafkaAdmin> kafkaAdmin;
@Autowired
private Optional<TbKafkaTopicConfigs> kafkaTopicConfigs;
private Server server; private Server server;
@ -232,8 +228,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
private EdgeGrpcSession createEdgeGrpcSession(StreamObserver<ResponseMsg> outputStream) { private EdgeGrpcSession createEdgeGrpcSession(StreamObserver<ResponseMsg> outputStream) {
return kafkaSettings.isPresent() && kafkaTopicConfigs.isPresent() return kafkaAdmin.isPresent()
? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaSettings.get(), kafkaTopicConfigs.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect, ? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaAdmin.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect,
sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession) sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession)
: new PostgresEdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, : new PostgresEdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect,
sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession);
@ -643,10 +639,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
List<EdgeId> toRemove = new ArrayList<>(); List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) { for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession && if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() && !kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null && kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null && kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) { !kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId()); toRemove.add(kafkaSession.getEdge().getId());
} }
} }
@ -663,4 +659,5 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.warn("Failed to cleanup kafka sessions", e); log.warn("Failed to cleanup kafka sessions", e);
} }
} }
} }

15
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -298,6 +299,10 @@ public abstract class EdgeGrpcSession implements Closeable {
processHighPriorityEvents(); processHighPriorityEvents();
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
if (fetcher instanceof GeneralEdgeEventFetcher) {
long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize));
}
log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@ -461,6 +466,8 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
} }
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), edge.getId(), 1));
log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt);
log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy);
} }
@ -474,6 +481,8 @@ public abstract class EdgeGrpcSession implements Closeable {
log.error("[{}][{}][{}] {} Message {}", tenantId, edge.getId(), sessionId, message, downlinkMsg); log.error("[{}][{}][{}] {} Message {}", tenantId, edge.getId(), sessionId, message, downlinkMsg);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build());
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), 1));
sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId());
} else { } else {
sendDownlinkMsg(ResponseMsg.newBuilder() sendDownlinkMsg(ResponseMsg.newBuilder()
@ -490,6 +499,7 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size()));
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} else { } else {
@ -529,6 +539,7 @@ public abstract class EdgeGrpcSession implements Closeable {
try { try {
if (msg.getSuccess()) { if (msg.getSuccess()) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1));
log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else { } else {
log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
@ -649,7 +660,8 @@ public abstract class EdgeGrpcSession implements Closeable {
log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg); log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg);
} }
} }
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED ->
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);
default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction()); default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction());
} }
} catch (Exception e) { } catch (Exception e) {
@ -795,6 +807,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} }
highPriorityQueue.add(edgeEvent); highPriorityQueue.add(edgeEvent);
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1));
} }
protected ListenableFuture<List<Void>> processUplinkMsg(UplinkMsg uplinkMsg) { protected ListenableFuture<List<Void>> processUplinkMsg(UplinkMsg uplinkMsg) {

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java

@ -25,11 +25,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.edge.BaseEdgeEventService; import org.thingsboard.server.dao.edge.BaseEdgeEventService;
import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
@ -40,6 +43,7 @@ public class KafkaEdgeEventService extends BaseEdgeEventService {
private final TopicService topicService; private final TopicService topicService;
private final TbQueueProducerProvider producerProvider; private final TbQueueProducerProvider producerProvider;
private final Optional<EdgeStatsCounterService> statsCounterService;
@Override @Override
public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) { public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) {
@ -48,7 +52,7 @@ public class KafkaEdgeEventService extends BaseEdgeEventService {
TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId());
ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build(); ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build();
producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null);
statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1));
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }

16
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -32,9 +32,7 @@ import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.KafkaAdmin;
import org.thingsboard.server.queue.kafka.TbKafkaSettings;
import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeContextComponent;
@ -51,9 +49,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
private final TopicService topicService; private final TopicService topicService;
private final TbCoreQueueFactory tbCoreQueueFactory; private final TbCoreQueueFactory tbCoreQueueFactory;
private final KafkaAdmin kafkaAdmin;
private final TbKafkaSettings kafkaSettings;
private final TbKafkaTopicConfigs kafkaTopicConfigs;
private volatile boolean isHighPriorityProcessing; private volatile boolean isHighPriorityProcessing;
@ -63,21 +59,20 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
private ExecutorService consumerExecutor; private ExecutorService consumerExecutor;
public KafkaEdgeGrpcSession(EdgeContextComponent ctx, TopicService topicService, TbCoreQueueFactory tbCoreQueueFactory, public KafkaEdgeGrpcSession(EdgeContextComponent ctx, TopicService topicService, TbCoreQueueFactory tbCoreQueueFactory,
TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs, StreamObserver<ResponseMsg> outputStream, KafkaAdmin kafkaAdmin, StreamObserver<ResponseMsg> outputStream,
BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener, BiConsumer<Edge, UUID> sessionCloseListener, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener, BiConsumer<Edge, UUID> sessionCloseListener,
ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) {
super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession);
this.topicService = topicService; this.topicService = topicService;
this.tbCoreQueueFactory = tbCoreQueueFactory; this.tbCoreQueueFactory = tbCoreQueueFactory;
this.kafkaSettings = kafkaSettings; this.kafkaAdmin = kafkaAdmin;
this.kafkaTopicConfigs = kafkaTopicConfigs;
} }
private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) { private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) {
log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId()); log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId());
if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " +
"connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration",
tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing); tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing);
return; return;
} }
@ -159,7 +154,6 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override @Override
public void cleanUp() { public void cleanUp() {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic();
TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs());
kafkaAdmin.deleteTopic(topic); kafkaAdmin.deleteTopic(topic);
kafkaAdmin.deleteConsumerGroup(topic); kafkaAdmin.deleteConsumerGroup(topic);
} }

150
application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java

@ -0,0 +1,150 @@
/**
* Copyright © 2016-2025 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.edge.stats;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService;
import org.thingsboard.server.dao.edge.stats.MsgCounters;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.kafka.KafkaAdmin;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED;
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_LAG;
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED;
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED;
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED;
@TbCoreComponent
@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false)
@RequiredArgsConstructor
@Service
@Slf4j
public class EdgeStatsService {
private final TimeseriesService tsService;
private final EdgeStatsCounterService statsCounterService;
private final TopicService topicService;
private final Optional<KafkaAdmin> kafkaAdmin;
@Value("${edges.stats.ttl:30}")
private int edgesStatsTtlDays;
@Value("${edges.stats.report-interval-millis:600000}")
private long reportIntervalMillis;
@Scheduled(
fixedDelayString = "${edges.stats.report-interval-millis:600000}",
initialDelayString = "${edges.stats.report-interval-millis:600000}"
)
public void reportStats() {
log.debug("Reporting Edge communication stats...");
long now = System.currentTimeMillis();
long ts = now - (now % reportIntervalMillis);
Map<EdgeId, MsgCounters> countersByEdge = statsCounterService.getCounterByEdge();
Map<EdgeId, Long> lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap();
Map<EdgeId, MsgCounters> countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge());
countersByEdgeSnapshot.forEach((edgeId, counters) -> {
TenantId tenantId = counters.getTenantId();
if (kafkaAdmin.isPresent()) {
counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L));
}
List<TsKvEntry> statsEntries = List.of(
entry(ts, DOWNLINK_MSGS_ADDED.getKey(), counters.getMsgsAdded().get()),
entry(ts, DOWNLINK_MSGS_PUSHED.getKey(), counters.getMsgsPushed().get()),
entry(ts, DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey(), counters.getMsgsPermanentlyFailed().get()),
entry(ts, DOWNLINK_MSGS_TMP_FAILED.getKey(), counters.getMsgsTmpFailed().get()),
entry(ts, DOWNLINK_MSGS_LAG.getKey(), counters.getMsgsLag().get())
);
log.trace("Reported Edge communication stats: {} tenantId - {}, edgeId - {}", statsEntries, tenantId, edgeId);
saveTs(tenantId, edgeId, statsEntries);
});
}
private Map<EdgeId, Long> getEdgeLagByEdgeId(Map<EdgeId, MsgCounters> countersByEdge) {
Map<EdgeId, String> edgeToTopicMap = countersByEdge.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getTenantId(), e.getKey()).getTopic()
));
Map<String, Long> lagByTopic = kafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values()));
return edgeToTopicMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> lagByTopic.getOrDefault(e.getValue(), 0L)
));
}
private void saveTs(TenantId tenantId, EdgeId edgeId, List<TsKvEntry> statsEntries) {
try {
ListenableFuture<TimeseriesSaveResult> future = tsService.save(
tenantId,
edgeId,
statsEntries,
TimeUnit.DAYS.toSeconds(edgesStatsTtlDays)
);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(TimeseriesSaveResult result) {
log.debug("Successfully saved edge time-series stats: {} for edge: {}", statsEntries, edgeId);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to save edge time-series stats for edge: {}", edgeId, t);
}
}, MoreExecutors.directExecutor());
} finally {
statsCounterService.clear(edgeId);
}
}
private BasicTsKvEntry entry(long ts, String key, long value) {
return new BasicTsKvEntry(ts, new LongDataEntry(key, value));
}
}

7
application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java

@ -20,10 +20,8 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsConfig;
import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.KafkaAdmin;
import org.thingsboard.server.queue.kafka.TbKafkaSettings;
import java.util.Collections;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -33,8 +31,7 @@ public class KafkaEdqsSyncService extends EdqsSyncService {
private final boolean syncNeeded; private final boolean syncNeeded;
public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings, TopicService topicService, EdqsConfig edqsConfig) { public KafkaEdqsSyncService(KafkaAdmin kafkaAdmin, TopicService topicService, EdqsConfig edqsConfig) {
TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap());
this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions()) this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions())
.mapToObj(partition -> TopicPartitionInfo.builder() .mapToObj(partition -> TopicPartitionInfo.builder()
.topic(topicService.buildTopicName(edqsConfig.getEventsTopic())) .topic(topicService.buildTopicName(edqsConfig.getEventsTopic()))

4
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -97,7 +97,7 @@ public abstract class AbstractTbEntityService {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
} }
protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) {
if (vcService != null) { if (vcService != null) {
return vcService.autoCommit(user, entityId); return vcService.autoCommit(user, entityId);
} else { } else {
@ -106,7 +106,7 @@ public abstract class AbstractTbEntityService {
} }
} }
protected ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception { protected ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) {
if (vcService != null) { if (vcService != null) {
return vcService.autoCommit(user, entityType, entityIds); return vcService.autoCommit(user, entityType, entityIds);
} else { } else {

80
application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2025 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.entitiy.ai;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import static java.util.Objects.requireNonNullElseGet;
@Service
@TbCoreComponent
@RequiredArgsConstructor
class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelService aiModelService;
@Override
public AiModel save(AiModel model, User user) {
var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId();
model.setTenantId(tenantId);
AiModel savedModel;
try {
savedModel = aiModelService.save(model);
autoCommit(user, savedModel.getId());
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e);
throw e;
}
logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedModel;
}
@Override
public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED;
var tenantId = user.getTenantId();
var modelId = model.getId();
boolean deleted;
try {
deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString());
throw e;
}
if (deleted) {
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
}
return deleted;
}
}

27
application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2025 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.entitiy.ai;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
public interface TbAiModelService {
AiModel save(AiModel model, User user);
boolean delete(AiModel model, User user);
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService {
@Autowired @Autowired
private AlarmCommentService alarmCommentService; private AlarmCommentService alarmCommentService;
@ -68,4 +68,5 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem
throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
} }
} }

4
application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java

@ -176,8 +176,8 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb
for (int i = oldPartitions; i < newPartitions; i++) { for (int i = oldPartitions; i < newPartitions; i++) {
tbQueueAdmin.createTopicIfNotExists( tbQueueAdmin.createTopicIfNotExists(
new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(),
queue.getCustomProperties() queue.getCustomProperties(),
); true); // forcing topic creation because the topic may still be cached on some nodes
} }
} }

43
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java

@ -43,33 +43,30 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor<Alarms
EntityType entityType = entityId.getEntityType(); EntityType entityType = entityId.getEntityType();
TenantId tenantId = task.getTenantId(); TenantId tenantId = task.getTenantId();
if (entityType == EntityType.DEVICE || entityType == EntityType.ASSET) { if (task.getAlarms() == null) {
if (task.getAlarms() == null) { AlarmId lastId = null;
AlarmId lastId = null; long lastCreatedTime = 0;
long lastCreatedTime = 0; while (true) {
while (true) { List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128);
List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); if (alarms.isEmpty()) {
if (alarms.isEmpty()) { break;
break; }
}
housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList()));
TbPair<UUID, Long> last = alarms.get(alarms.size() - 1); TbPair<UUID, Long> last = alarms.get(alarms.size() - 1);
lastId = new AlarmId(last.getFirst()); lastId = new AlarmId(last.getFirst());
lastCreatedTime = last.getSecond(); lastCreatedTime = last.getSecond();
log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size());
}
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} }
@Override @Override

183
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -22,9 +22,9 @@ import freemarker.template.Template;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -64,55 +64,37 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@Service
@Slf4j @Slf4j
@Service
@RequiredArgsConstructor
public class DefaultMailService implements MailService { public class DefaultMailService implements MailService {
public static final String TARGET_EMAIL = "targetEmail"; private static final String TARGET_EMAIL = "targetEmail";
public static final String UTF_8 = "UTF-8"; private static final String UTF_8 = "UTF-8";
private static final long DEFAULT_TIMEOUT = 10_000;
private final ScheduledExecutorService timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog");
private final MessageSource messages; private final MessageSource messages;
private final Configuration freemarkerConfig; private final Configuration freemarkerConfig;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private static final long DEFAULT_TIMEOUT = 10_000;
@Lazy @Lazy
@Autowired private final TbApiUsageStateService apiUsageStateService;
private TbApiUsageStateService apiUsageStateService; private final MailSenderInternalExecutorService mailExecutorService;
private final PasswordResetExecutorService passwordResetExecutorService;
@Autowired private final TbMailContextComponent ctx;
private MailSenderInternalExecutorService mailExecutorService; private final RateLimitService rateLimitService;
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
@Autowired
private TbMailContextComponent ctx;
@Autowired
private RateLimitService rateLimitService;
@Value("${mail.per_tenant_rate_limits:}") @Value("${mail.per_tenant_rate_limits:}")
private String perTenantRateLimitConfig; private String perTenantRateLimitConfig;
private final ScheduledExecutorService timeoutScheduler;
private TbMailSender mailSender; private TbMailSender mailSender;
private String mailFrom; private String mailFrom;
private long timeout; private long timeout;
public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) {
this.messages = messages;
this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService;
this.apiUsageClient = apiUsageClient;
this.timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog");
}
@PostConstruct @PostConstruct
private void init() { private void init() {
updateMailConfiguration(); updateMailConfiguration();
@ -120,9 +102,7 @@ public class DefaultMailService implements MailService {
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if (timeoutScheduler != null) { timeoutScheduler.shutdownNow();
timeoutScheduler.shutdownNow();
}
} }
@Override @Override
@ -311,22 +291,21 @@ public class DefaultMailService implements MailService {
model.put("apiFeature", apiFeature.getLabel()); model.put("apiFeature", apiFeature.getLabel());
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = null; String message = switch (stateValue) {
case ENABLED -> {
switch (stateValue) {
case ENABLED:
model.put("apiLabel", toEnabledValueLabel(apiFeature)); model.put("apiLabel", toEnabledValueLabel(apiFeature));
message = mergeTemplateIntoString("state.enabled.ftl", model); yield mergeTemplateIntoString("state.enabled.ftl", model);
break; }
case WARNING: case WARNING -> {
model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState));
message = mergeTemplateIntoString("state.warning.ftl", model); yield mergeTemplateIntoString("state.warning.ftl", model);
break; }
case DISABLED: case DISABLED -> {
model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState));
message = mergeTemplateIntoString("state.disabled.ftl", model); yield mergeTemplateIntoString("state.disabled.ftl", model);
break; }
} };
sendMail(mailSender, mailFrom, email, subject, message, timeout); sendMail(mailSender, mailFrom, email, subject, message, timeout);
} }
@ -341,89 +320,55 @@ public class DefaultMailService implements MailService {
} }
private String toEnabledValueLabel(ApiFeature apiFeature) { private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { return switch (apiFeature) {
case DB: case DB -> "save";
return "save"; case TRANSPORT -> "receive";
case TRANSPORT: case JS -> "invoke";
return "receive"; case RE -> "process";
case JS: case EMAIL, SMS -> "send";
return "invoke"; case ALARM -> "create";
case RE: default -> throw new RuntimeException("Not implemented!");
return "process"; };
case EMAIL:
case SMS:
return "send";
case ALARM:
return "create";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toDisabledValueLabel(ApiFeature apiFeature) { private String toDisabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { return switch (apiFeature) {
case DB: case DB -> "saved";
return "saved"; case TRANSPORT -> "received";
case TRANSPORT: case JS -> "invoked";
return "received"; case RE -> "processed";
case JS: case EMAIL, SMS -> "sent";
return "invoked"; case ALARM -> "created";
case RE: default -> throw new RuntimeException("Not implemented!");
return "processed"; };
case EMAIL:
case SMS:
return "sent";
case ALARM:
return "created";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toWarningValueLabel(ApiUsageRecordState recordState) { private String toWarningValueLabel(ApiUsageRecordState recordState) {
String valueInM = recordState.getValueAsString(); String valueInM = recordState.getValueAsString();
String thresholdInM = recordState.getThresholdAsString(); String thresholdInM = recordState.getThresholdAsString();
switch (recordState.getKey()) { return switch (recordState.getKey()) {
case STORAGE_DP_COUNT: case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> valueInM + " out of " + thresholdInM + " allowed data points";
case TRANSPORT_DP_COUNT: case TRANSPORT_MSG_COUNT -> valueInM + " out of " + thresholdInM + " allowed messages";
return valueInM + " out of " + thresholdInM + " allowed data points"; case JS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed JavaScript functions";
case TRANSPORT_MSG_COUNT: case TBEL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Tbel functions";
return valueInM + " out of " + thresholdInM + " allowed messages"; case RE_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
case JS_EXEC_COUNT: case EMAIL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Email messages";
return valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; case SMS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed SMS messages";
case TBEL_EXEC_COUNT: default -> throw new RuntimeException("Not implemented!");
return valueInM + " out of " + thresholdInM + " allowed Tbel functions"; };
case RE_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
case EMAIL_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Email messages";
case SMS_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toDisabledValueLabel(ApiUsageRecordState recordState) { private String toDisabledValueLabel(ApiUsageRecordState recordState) {
switch (recordState.getKey()) { return switch (recordState.getKey()) {
case STORAGE_DP_COUNT: case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> recordState.getValueAsString() + " data points";
case TRANSPORT_DP_COUNT: case TRANSPORT_MSG_COUNT -> recordState.getValueAsString() + " messages";
return recordState.getValueAsString() + " data points"; case JS_EXEC_COUNT -> "JavaScript functions " + recordState.getValueAsString() + " times";
case TRANSPORT_MSG_COUNT: case TBEL_EXEC_COUNT -> "TBEL functions " + recordState.getValueAsString() + " times";
return recordState.getValueAsString() + " messages"; case RE_EXEC_COUNT -> recordState.getValueAsString() + " Rule Engine messages";
case JS_EXEC_COUNT: case EMAIL_EXEC_COUNT -> recordState.getValueAsString() + " Email messages";
return "JavaScript functions " + recordState.getValueAsString() + " times"; case SMS_EXEC_COUNT -> recordState.getValueAsString() + " SMS messages";
case TBEL_EXEC_COUNT: default -> throw new RuntimeException("Not implemented!");
return "TBEL functions " + recordState.getValueAsString() + " times"; };
case RE_EXEC_COUNT:
return recordState.getValueAsString() + " Rule Engine messages";
case EMAIL_EXEC_COUNT:
return recordState.getValueAsString() + " Email messages";
case SMS_EXEC_COUNT:
return recordState.getValueAsString() + " SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
} }
private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email,

22
application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java

@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.gson.GsonFactory;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.mail.MailException; import org.springframework.mail.MailException;
@ -50,8 +51,10 @@ public class TbMailSender extends JavaMailSenderImpl {
private final TbMailContextComponent ctx; private final TbMailContextComponent ctx;
private final Lock lock; private final Lock lock;
@Getter
private final Boolean oauth2Enabled; private final Boolean oauth2Enabled;
private volatile String accessToken; private volatile String accessToken;
@Getter
private volatile long tokenExpires; private volatile long tokenExpires;
public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) {
@ -70,14 +73,6 @@ public class TbMailSender extends JavaMailSenderImpl {
setJavaMailProperties(createJavaMailProperties(jsonConfig)); setJavaMailProperties(createJavaMailProperties(jsonConfig));
} }
public Boolean getOauth2Enabled() {
return oauth2Enabled;
}
public long getTokenExpires() {
return tokenExpires;
}
@Override @Override
protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
updateOauth2PasswordIfExpired(); updateOauth2PasswordIfExpired();
@ -98,8 +93,8 @@ public class TbMailSender extends JavaMailSenderImpl {
super.testConnection(); super.testConnection();
} }
public void updateOauth2PasswordIfExpired() { public void updateOauth2PasswordIfExpired() {
if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())) {
refreshAccessToken(); refreshAccessToken();
setPassword(accessToken); setPassword(accessToken);
} }
@ -168,8 +163,8 @@ public class TbMailSender extends JavaMailSenderImpl {
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute(); .execute();
if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) {
((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); ((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
} }
accessToken = tokenResponse.getAccessToken(); accessToken = tokenResponse.getAccessToken();
@ -190,4 +185,5 @@ public class TbMailSender extends JavaMailSenderImpl {
throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
} }
} }
}
}

6
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java

@ -140,8 +140,10 @@ public class DefaultFirebaseService implements FirebaseService {
} }
public void destroy() { public void destroy() {
app.delete(); if (app != null) {
app = null; app.delete();
app = null;
}
messaging = null; messaging = null;
log.debug("[{}] Destroyed FirebaseContext", key); log.debug("[{}] Destroyed FirebaseContext", key);
} }

96
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java

@ -15,20 +15,25 @@
*/ */
package org.thingsboard.server.service.notification.provider; package org.thingsboard.server.service.notification.provider;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import com.slack.api.Slack; import com.slack.api.Slack;
import com.slack.api.methods.MethodsClient; import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiRequest; import com.slack.api.methods.SlackApiRequest;
import com.slack.api.methods.SlackApiTextResponse; import com.slack.api.methods.SlackApiTextResponse;
import com.slack.api.methods.SlackFilesUploadV2Exception;
import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.request.conversations.ConversationsListRequest; import com.slack.api.methods.request.conversations.ConversationsListRequest;
import com.slack.api.methods.request.conversations.ConversationsOpenRequest;
import com.slack.api.methods.request.files.FilesUploadV2Request;
import com.slack.api.methods.request.users.UsersListRequest; import com.slack.api.methods.request.users.UsersListRequest;
import com.slack.api.methods.response.conversations.ConversationsListResponse; import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType; import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -36,6 +41,8 @@ import org.thingsboard.server.common.data.notification.settings.NotificationSett
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackFile;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -58,11 +65,40 @@ public class DefaultSlackService implements SlackService {
@Override @Override
public void sendMessage(TenantId tenantId, String token, String conversationId, String message) { public void sendMessage(TenantId tenantId, String token, String conversationId, String message) {
ChatPostMessageRequest request = ChatPostMessageRequest.builder() sendMessage(tenantId, token, conversationId, message, null);
.channel(conversationId) }
.text(message)
.build(); @Override
sendRequest(token, request, MethodsClient::chatPostMessage); public void sendMessage(TenantId tenantId, String token, String conversationId, String message, List<SlackFile> files) {
if (CollectionsUtil.isNotEmpty(files)) {
if (conversationId.startsWith("U")) { // direct message
/*
* files.uploadV2 requires an existing channel ID, while chat.postMessage autoopens DMs
* */
conversationId = sendRequest(token, ConversationsOpenRequest.builder()
.users(List.of(conversationId))
.build(), MethodsClient::conversationsOpen).getChannel().getId();
}
FilesUploadV2Request request = FilesUploadV2Request.builder()
.initialComment(message)
.channel(conversationId)
.uploadFiles(files.stream()
.map(file -> FilesUploadV2Request.UploadFile.builder()
.filename(file.getName())
.title(file.getName())
.fileData(file.getData())
.build())
.toList())
.build();
sendRequest(token, request, MethodsClient::filesUploadV2);
} else {
ChatPostMessageRequest request = ChatPostMessageRequest.builder()
.channel(conversationId)
.text(message)
.build();
sendRequest(token, request, MethodsClient::chatPostMessage);
}
} }
@Override @Override
@ -128,22 +164,52 @@ public class DefaultSlackService implements SlackService {
R response; R response;
try { try {
response = method.apply(client, request); response = method.apply(client, request);
} catch (SlackFilesUploadV2Exception e) {
if (e.getGetURLResponses() != null) {
e.getGetURLResponses().forEach(this::checkResponse);
}
if (e.getCompleteResponse() != null) {
checkResponse(e.getCompleteResponse());
}
if (e.getFileInfoResponses() != null) {
e.getFileInfoResponses().forEach(this::checkResponse);
}
throw new RuntimeException("Failed to upload Slack file: " + e.toString(), e);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e.getMessage(), e); throw new RuntimeException(e.getMessage(), e);
} }
if (!response.isOk()) { checkResponse(response);
String error = response.getError(); return response;
if (error == null) { }
error = "unknown error";
} else if (error.contains("missing_scope")) {
String neededScope = response.getNeeded(); private void checkResponse(SlackApiTextResponse response) {
error = "bot token scope '" + neededScope + "' is needed"; if (response.isOk()) {
} return;
throw new RuntimeException("Slack API error: " + error);
} }
return response; String error = response.getError();
if (error != null) {
switch (error) {
case "missing_scope" -> {
String neededScope = response.getNeeded();
error = "bot token scope '" + neededScope + "' is needed";
}
case "not_in_channel" -> {
error = "app needs to be added to the channel";
}
default -> {
error = null;
}
}
}
if (error == null) {
ObjectNode responseJson = (ObjectNode) JacksonUtil.valueToTree(response);
responseJson.remove("httpResponseHeaders");
error = responseJson.toString();
}
throw new RuntimeException("Slack API error: " + error);
} }
} }

5
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -57,6 +57,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.DbCallbackExecutorService;
@ -224,7 +225,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) { private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY))); new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME)));
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
} }
@ -232,7 +233,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder();
EntityDataSortOrder entitiesSortOrder; EntityDataSortOrder entitiesSortOrder;
if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) {
entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)); entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME));
} else { } else {
entitiesSortOrder = sortOrder; entitiesSortOrder = sortOrder;
} }

23
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -583,17 +583,18 @@ public class DefaultTbClusterService implements TbClusterService {
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);
EntityType entityType = msg.getEntityId().getEntityType(); EntityType entityType = msg.getEntityId().getEntityType();
if (entityType.equals(EntityType.TENANT) if (entityType.isOneOf(
|| entityType.equals(EntityType.TENANT_PROFILE) EntityType.TENANT,
|| entityType.equals(EntityType.DEVICE_PROFILE) EntityType.API_USAGE_STATE,
|| (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) EntityType.ENTITY_VIEW,
|| entityType.equals(EntityType.ASSET_PROFILE) EntityType.NOTIFICATION_RULE,
|| entityType.equals(EntityType.API_USAGE_STATE) EntityType.CALCULATED_FIELD,
|| (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) EntityType.TENANT_PROFILE,
|| entityType.equals(EntityType.ENTITY_VIEW) EntityType.DEVICE_PROFILE,
|| entityType.equals(EntityType.NOTIFICATION_RULE) EntityType.ASSET_PROFILE,
|| entityType.equals(EntityType.CALCULATED_FIELD) EntityType.JOB)
|| entityType.equals(EntityType.JOB) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
) { ) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);

100
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -17,18 +17,16 @@ package org.thingsboard.server.service.script;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory; import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -36,8 +34,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@Slf4j
public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService, JsonNode> { public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService, JsonNode> {
public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) { public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) {
@ -45,87 +41,81 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService
} }
@Override @Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) { protected Object[] prepareArgs(TbMsg msg) {
return executeScriptAsync(msg); String[] args = new String[3];
if (msg.getData() != null) {
args[0] = msg.getData();
} else {
args[0] = "";
}
args[1] = JacksonUtil.toString(msg.getMetaData().getData());
args[2] = msg.getType();
return args;
} }
@Override @Override
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) { protected List<TbMsg> executeUpdateTransform(TbMsg msg, JsonNode json) {
if (json.isObject()) { if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); return Collections.singletonList(unbindMsg(json, msg));
} else if (json.isArray()) { } else if (json.isArray()) {
List<TbMsg> res = new ArrayList<>(json.size()); List<TbMsg> res = new ArrayList<>(json.size());
json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return Futures.immediateFuture(res); return res;
} }
log.warn("Wrong result type: {}", json.getNodeType()); throw wrongResultType(json);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} }
@Override @Override
protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) { protected TbMsg executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
if (!result.isObject()) { if (!result.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType()); throw wrongResultType(result);
Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
return Futures.immediateFuture(unbindMsg(result, prevMsg));
}
@Override
protected JsonNode convertResult(Object result) {
return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
}
@Override
protected ListenableFuture<String> executeToStringTransform(JsonNode result) {
if (result.isTextual()) {
return Futures.immediateFuture(result.asText());
} }
log.warn("Wrong result type: {}", result.getNodeType()); return unbindMsg(result, prevMsg);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
} }
@Override @Override
protected ListenableFuture<Boolean> executeFilterTransform(JsonNode json) { protected boolean executeFilterTransform(JsonNode json) {
if (json.isBoolean()) { if (json.isBoolean()) {
return Futures.immediateFuture(json.asBoolean()); return json.asBoolean();
} }
log.warn("Wrong result type: {}", json.getNodeType()); throw wrongResultType(json);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} }
@Override @Override
protected ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) { protected Set<String> executeSwitchTransform(JsonNode result) {
if (result.isTextual()) { if (result.isTextual()) {
return Futures.immediateFuture(Collections.singleton(result.asText())); return Collections.singleton(result.asText());
} }
if (result.isArray()) { if (result.isArray()) {
Set<String> nextStates = new HashSet<>(); Set<String> nextStates = new HashSet<>();
for (JsonNode val : result) { for (JsonNode val : result) {
if (!val.isTextual()) { if (!val.isTextual()) {
log.warn("Wrong result type: {}", val.getNodeType()); throw wrongResultType(val);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType()));
} else { } else {
nextStates.add(val.asText()); nextStates.add(val.asText());
} }
} }
return Futures.immediateFuture(nextStates); return nextStates;
} }
log.warn("Wrong result type: {}", result.getNodeType()); throw wrongResultType(result);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
} }
@Override @Override
protected Object[] prepareArgs(TbMsg msg) { public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
String[] args = new String[3]; return executeScriptAsync(msg);
if (msg.getData() != null) { }
args[0] = msg.getData();
} else { @Override
args[0] = ""; protected String executeToStringTransform(JsonNode result) {
if (result.isTextual()) {
return result.asText();
} }
args[1] = JacksonUtil.toString(msg.getMetaData().getData()); throw wrongResultType(result);
args[2] = msg.getType(); }
return args;
@Override
protected JsonNode convertResult(Object result) {
return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
} }
private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) { private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
@ -138,19 +128,23 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService
} }
if (msgData.has(RuleNodeScriptFactory.METADATA)) { if (msgData.has(RuleNodeScriptFactory.METADATA)) {
JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA); JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() { metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() {});
});
} }
if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) { if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText(); messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
} }
String newData = data != null ? data : msg.getData(); String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType();
return msg.transform() return msg.transform()
.type(newMessageType) .type(newMessageType)
.metaData(newMetadata) .metaData(newMetadata)
.data(newData) .data(newData)
.build(); .build();
} }
private TbScriptException wrongResultType(JsonNode result) {
return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + result.getNodeType()));
}
} }

70
application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java

@ -17,41 +17,44 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j @Slf4j
public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> implements ScriptEngine { public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> implements ScriptEngine {
private final T scriptInvokeService; private final T scriptInvokeService;
private final UUID scriptId; protected final UUID scriptId;
private final TenantId tenantId; private final TenantId tenantId;
public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) { public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.scriptInvokeService = scriptInvokeService; this.scriptInvokeService = scriptInvokeService;
try { try {
this.scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) { } catch (Exception e) {
Throwable t = e; Throwable t = e;
if (e instanceof ExecutionException) { if (e instanceof ExecutionException) {
t = e.getCause(); t = e.getCause();
} }
throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t); if (t instanceof TbScriptException scriptException) {
throw scriptException;
}
throw new RuntimeException("Unexpected error when creating script engine: " + t.getMessage(), t);
} }
} }
@ -60,74 +63,53 @@ public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> imp
@Override @Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<R> result = executeScriptAsync(msg); ListenableFuture<R> result = executeScriptAsync(msg);
return Futures.transformAsync(result, return Futures.transform(result, json -> executeUpdateTransform(msg, json), directExecutor());
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
} }
protected abstract ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, R result); protected abstract List<TbMsg> executeUpdateTransform(TbMsg msg, R result);
@Override @Override
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) { public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
return Futures.transformAsync(executeScriptAsync(prevMsg), return Futures.transform(executeScriptAsync(prevMsg), result -> executeGenerateTransform(prevMsg, result), directExecutor());
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
} }
protected abstract ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, R result); protected abstract TbMsg executeGenerateTransform(TbMsg prevMsg, R result);
@Override @Override
public ListenableFuture<String> executeToStringAsync(TbMsg msg) { public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor()); return Futures.transform(executeScriptAsync(msg), this::executeFilterTransform, directExecutor());
} }
protected abstract boolean executeFilterTransform(R result);
@Override @Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg), return Futures.transform(executeScriptAsync(msg), this::executeSwitchTransform, directExecutor()); // usually runs on a callbackExecutor
this::executeFilterTransform,
MoreExecutors.directExecutor());
} }
protected abstract ListenableFuture<String> executeToStringTransform(R result); protected abstract Set<String> executeSwitchTransform(R result);
protected abstract ListenableFuture<Boolean> executeFilterTransform(R result);
protected abstract ListenableFuture<Set<String>> executeSwitchTransform(R result);
@Override @Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) { public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg), return Futures.transform(executeScriptAsync(msg), this::executeToStringTransform, directExecutor());
this::executeSwitchTransform,
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
} }
protected abstract String executeToStringTransform(R result);
ListenableFuture<R> executeScriptAsync(TbMsg msg) { ListenableFuture<R> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg); log.trace("execute script async, msg {}", msg);
Object[] inArgs = prepareArgs(msg); Object[] inArgs = prepareArgs(msg);
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
} }
ListenableFuture<R> executeScriptAsync(CustomerId customerId, Object... args) { private ListenableFuture<R> executeScriptAsync(CustomerId customerId, Object... args) {
return Futures.transformAsync(scriptInvokeService.invokeScript(tenantId, customerId, this.scriptId, args), return Futures.transform(scriptInvokeService.invokeScript(tenantId, customerId, scriptId, args), this::convertResult, directExecutor());
o -> {
try {
return Futures.immediateFuture(convertResult(o));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());
} else if (e.getCause() instanceof RuntimeException) {
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
} else {
return Futures.immediateFailedFuture(new ScriptException(e));
}
}
}, MoreExecutors.directExecutor());
} }
public void destroy() { public void destroy() {
scriptInvokeService.release(this.scriptId); scriptInvokeService.release(scriptId);
} }
protected abstract R convertResult(Object result); protected abstract R convertResult(Object result);
} }

112
application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java

@ -19,17 +19,15 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory; import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -40,8 +38,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> { public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> {
public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) { public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) {
@ -49,70 +47,74 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
} }
@Override @Override
protected ListenableFuture<Boolean> executeFilterTransform(Object result) { protected Object[] prepareArgs(TbMsg msg) {
if (result instanceof Boolean) { Object[] args = new Object[3];
return Futures.immediateFuture((Boolean) result); if (msg.getData() != null) {
args[0] = JacksonUtil.fromString(msg.getData(), Object.class);
} else {
args[0] = new HashMap<>();
} }
return wrongResultType(result); args[1] = new HashMap<>(msg.getMetaData().getData());
args[2] = msg.getType();
return args;
} }
@Override @Override
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, Object result) { protected List<TbMsg> executeUpdateTransform(TbMsg msg, Object result) {
if (result instanceof Map) { if (result instanceof Map msgData) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg((Map) result, msg))); return Collections.singletonList(unbindMsg(msgData, msg));
} else if (result instanceof Collection) { } else if (result instanceof Collection resultCollection) {
List<TbMsg> res = new ArrayList<>(); List<TbMsg> res = new ArrayList<>(resultCollection.size());
for (Object resObject : (Collection) result) { for (Object resObject : resultCollection) {
if (resObject instanceof Map) { if (resObject instanceof Map msgData) {
res.add(unbindMsg((Map) resObject, msg)); res.add(unbindMsg(msgData, msg));
} else { } else {
return wrongResultType(resObject); throw wrongResultType(resObject);
} }
} }
return Futures.immediateFuture(res); return res;
} }
return wrongResultType(result); throw wrongResultType(result);
} }
@Override @Override
protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, Object result) { protected TbMsg executeGenerateTransform(TbMsg prevMsg, Object result) {
if (result instanceof Map) { if (result instanceof Map msgData) {
return Futures.immediateFuture(unbindMsg((Map) result, prevMsg)); return unbindMsg(msgData, prevMsg);
} }
return wrongResultType(result); throw wrongResultType(result);
} }
@Override @Override
protected ListenableFuture<String> executeToStringTransform(Object result) { protected boolean executeFilterTransform(Object result) {
if (result instanceof String) { if (result instanceof Boolean b) {
return Futures.immediateFuture((String) result); return b;
} else {
return Futures.immediateFuture(JacksonUtil.toString(result));
} }
throw wrongResultType(result);
} }
@Override @Override
protected ListenableFuture<Set<String>> executeSwitchTransform(Object result) { protected Set<String> executeSwitchTransform(Object result) {
if (result instanceof String) { if (result instanceof String str) {
return Futures.immediateFuture(Collections.singleton((String) result)); return Collections.singleton(str);
} else if (result instanceof Collection) { }
Set<String> res = new HashSet<>(); if (result instanceof Collection<?> resultCollection) {
for (Object resObject : (Collection) result) { Set<String> res = new HashSet<>(resultCollection.size());
if (resObject instanceof String) { for (Object resObject : resultCollection) {
res.add((String) resObject); if (resObject instanceof String str) {
res.add(str);
} else { } else {
return wrongResultType(resObject); throw wrongResultType(resObject);
} }
} }
return Futures.immediateFuture(res); return res;
} }
return wrongResultType(result); throw wrongResultType(result);
} }
@Override @Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) { public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, MoreExecutors.directExecutor()); return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, directExecutor());
} }
@Override @Override
@ -121,16 +123,8 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
} }
@Override @Override
protected Object[] prepareArgs(TbMsg msg) { protected String executeToStringTransform(Object result) {
Object[] args = new Object[3]; return result instanceof String str ? str : JacksonUtil.toString(result);
if (msg.getData() != null) {
args[0] = JacksonUtil.fromString(msg.getData(), Object.class);
} else {
args[0] = new HashMap<>();
}
args[1] = new HashMap<>(msg.getMetaData().getData());
args[2] = msg.getType();
return args;
} }
private static TbMsg unbindMsg(Map msgData, TbMsg msg) { private static TbMsg unbindMsg(Map msgData, TbMsg msg) {
@ -142,12 +136,12 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
} }
if (msgData.containsKey(RuleNodeScriptFactory.METADATA)) { if (msgData.containsKey(RuleNodeScriptFactory.METADATA)) {
Object msgMetadataObj = msgData.get(RuleNodeScriptFactory.METADATA); Object msgMetadataObj = msgData.get(RuleNodeScriptFactory.METADATA);
if (msgMetadataObj instanceof Map) { if (msgMetadataObj instanceof Map<?, ?> msgMetadataObjAsMap) {
metadata = ((Map<?, ?>) msgMetadataObj).entrySet().stream().filter(e -> e.getValue() != null) metadata = msgMetadataObjAsMap.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString())); .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()));
} else { } else {
metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() { metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {});
});
} }
} }
if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) { if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) {
@ -155,7 +149,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
} }
String newData = data != null ? data : msg.getData(); String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType();
return msg.transform() return msg.transform()
.type(newMessageType) .type(newMessageType)
.metaData(newMetadata) .metaData(newMetadata)
@ -163,13 +157,13 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
.build(); .build();
} }
private static <T> ListenableFuture<T> wrongResultType(Object result) { private TbScriptException wrongResultType(Object result) {
String className = toClassName(result); String className = toClassName(result);
log.warn("Wrong result type: {}", className); return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + className));
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className));
} }
private static String toClassName(Object result) { private static String toClassName(Object result) {
return result != null ? result.getClass().getSimpleName() : "null"; return result != null ? result.getClass().getSimpleName() : "null";
} }
} }

3
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -121,7 +121,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
errorPrefix = "/login?loginError="; errorPrefix = "/login?loginError=";
} }
getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix +
URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8));
} }
} }
@ -138,4 +138,5 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
} }
return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken(); return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken();
} }
} }

7
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -21,7 +21,8 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
public enum Resource { public enum Resource {
ADMIN_SETTINGS(),
ADMIN_SETTINGS(EntityType.ADMIN_SETTINGS),
ALARM(EntityType.ALARM), ALARM(EntityType.ALARM),
DEVICE(EntityType.DEVICE), DEVICE(EntityType.DEVICE),
ASSET(EntityType.ASSET), ASSET(EntityType.ASSET),
@ -51,7 +52,8 @@ public enum Resource {
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS, MOBILE_APP_SETTINGS,
JOB(EntityType.JOB); JOB(EntityType.JOB),
AI_MODEL(EntityType.AI_MODEL);
private final Set<EntityType> entityTypes; private final Set<EntityType> entityTypes;
@ -75,4 +77,5 @@ public enum Resource {
} }
throw new IllegalArgumentException("Unknown EntityType: " + entityType.name()); throw new IllegalArgumentException("Unknown EntityType: " + entityType.name());
} }
} }

17
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.security.permission;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -56,6 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker);
put(Resource.AI_MODEL, aiModelPermissionChecker);
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@ -146,4 +149,18 @@ public class TenantAdminPermissions extends AbstractPermissions {
}; };
private static final PermissionChecker<AiModelId, AiModel> aiModelPermissionChecker = new PermissionChecker<>() {
@Override
public boolean hasPermission(SecurityUser user, Operation operation) {
return true;
}
@Override
public boolean hasPermission(SecurityUser user, Operation operation, AiModelId entityId, AiModel entity) {
return user.getTenantId().equals(entity.getTenantId());
}
};
} }

16
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java

@ -31,16 +31,12 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory {
@Override @Override
public SmsSender createSmsSender(SmsProviderConfiguration config) { public SmsSender createSmsSender(SmsProviderConfiguration config) {
switch (config.getType()) { return switch (config.getType()) {
case AWS_SNS: case AWS_SNS -> new AwsSmsSender((AwsSnsSmsProviderConfiguration) config);
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); case TWILIO -> new TwilioSmsSender((TwilioSmsProviderConfiguration) config);
case TWILIO: case SMPP -> new SmppSmsSender((SmppSmsProviderConfiguration) config);
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); default -> throw new RuntimeException("Unknown SMS provider type " + config.getType());
case SMPP: };
return new SmppSmsSender((SmppSmsProviderConfiguration) config);
default:
throw new RuntimeException("Unknown SMS provider type " + config.getType());
}
} }
} }

12
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.sms;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException; import org.springframework.core.NestedRuntimeException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -37,8 +38,9 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
@Service
@Slf4j @Slf4j
@Service
@RequiredArgsConstructor
public class DefaultSmsService implements SmsService { public class DefaultSmsService implements SmsService {
private final SmsSenderFactory smsSenderFactory; private final SmsSenderFactory smsSenderFactory;
@ -48,13 +50,6 @@ public class DefaultSmsService implements SmsService {
private SmsSender smsSender; private SmsSender smsSender;
public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageReportClient apiUsageClient) {
this.smsSenderFactory = smsSenderFactory;
this.adminSettingsService = adminSettingsService;
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
}
@PostConstruct @PostConstruct
private void init() { private void init() {
updateSmsConfiguration(); updateSmsConfiguration();
@ -148,4 +143,5 @@ public class DefaultSmsService implements SmsService {
return new ThingsboardException(String.format("Unable to send SMS: %s", message), return new ThingsboardException(String.format("Unable to send SMS: %s", message),
ThingsboardErrorCode.GENERAL); ThingsboardErrorCode.GENERAL);
} }
} }

12
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.data.query.FilterPredicateType; import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.KeyFilterPredicate;
@ -94,9 +95,14 @@ public abstract class TbAbstractEntityQuerySubCtx<T extends EntityCountQuery> ex
public void setAndResolveQuery(T query) { public void setAndResolveQuery(T query) {
dynamicValues.clear(); dynamicValues.clear();
this.query = query; this.query = query;
if (query != null && query.getKeyFilters() != null) { if (query != null) {
for (KeyFilter filter : query.getKeyFilters()) { if (query.getEntityFilter() != null) {
registerDynamicValues(filter.getPredicate()); EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), getUserId(), getOwnerId());
}
if (query.getKeyFilters() != null) {
for (KeyFilter filter : query.getKeyFilters()) {
registerDynamicValues(filter.getPredicate());
}
} }
} }
resolve(getTenantId(), getCustomerId(), getUserId()); resolve(getTenantId(), getCustomerId(), getUserId());

5
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -107,6 +107,11 @@ public abstract class TbAbstractSubCtx {
return sessionRef.getSecurityCtx().getId(); return sessionRef.getSecurityCtx().getId();
} }
public EntityId getOwnerId() {
var customerId = getCustomerId();
return customerId != null && !customerId.isNullUid() ? customerId : getTenantId();
}
public void sendWsMsg(CmdUpdate update) { public void sendWsMsg(CmdUpdate update) {
wsLock.lock(); wsLock.lock();
try { try {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save