Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/2fa-enforce

pull/14043/head
VIacheslavKlimov 1 year ago
parent
commit
942f909260
  1. 175
      README.md
  2. 44
      application/pom.xml
  3. 14
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md
  4. 2
      application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg
  5. 2
      application/src/main/data/json/system/scada_symbols/battery-hp.svg
  6. 2
      application/src/main/data/json/system/scada_symbols/conical-tank.svg
  7. 6
      application/src/main/data/json/system/scada_symbols/control-panel-hp.svg
  8. 9
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  9. 2
      application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg
  10. 2
      application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
  11. 4
      application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
  12. 6
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  13. 2
      application/src/main/data/json/system/scada_symbols/elevated-tank.svg
  14. 2
      application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg
  15. 2
      application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg
  16. 2
      application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
  17. 2
      application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg
  18. 2
      application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg
  19. 2
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  20. 2
      application/src/main/data/json/system/scada_symbols/large-conical-tank.svg
  21. 2
      application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
  22. 2
      application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
  23. 2
      application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
  24. 2
      application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
  25. 2
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  26. 2
      application/src/main/data/json/system/scada_symbols/left-heat-pump.svg
  27. 2
      application/src/main/data/json/system/scada_symbols/meter.svg
  28. 2
      application/src/main/data/json/system/scada_symbols/pool.svg
  29. 2
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  30. 2
      application/src/main/data/json/system/scada_symbols/right-heat-pump.svg
  31. 12
      application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg
  32. 12
      application/src/main/data/json/system/scada_symbols/sand-filter.svg
  33. 4
      application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
  34. 6
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  35. 2
      application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg
  36. 2
      application/src/main/data/json/system/scada_symbols/small-left-meter.svg
  37. 2
      application/src/main/data/json/system/scada_symbols/small-meter.svg
  38. 2
      application/src/main/data/json/system/scada_symbols/small-right-center.svg
  39. 2
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  40. 2
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  41. 2
      application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
  42. 2
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  43. 2
      application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
  44. 2
      application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
  45. 2
      application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg
  46. 2
      application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg
  47. 2
      application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg
  48. 2
      application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
  49. 2
      application/src/main/data/json/system/scada_symbols/vertical-tank.svg
  50. 2
      application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg
  51. 2
      application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg
  52. 3
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  53. 35
      application/src/main/data/json/system/widget_types/api_usage.json
  54. 4
      application/src/main/data/json/system/widget_types/rpc_debug_terminal.json
  55. 63
      application/src/main/data/upgrade/basic/schema_update.sql
  56. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  57. 3
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  58. 11
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java
  59. 5
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
  60. 56
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  61. 15
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  62. 240
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  63. 13
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java
  64. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java
  65. 11
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  66. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  67. 3
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  68. 4
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  69. 27
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  70. 61
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  71. 178
      application/src/main/java/org/thingsboard/server/controller/AiModelController.java
  72. 16
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  73. 60
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  74. 22
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  75. 110
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  76. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  77. 15
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  78. 35
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  79. 163
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  80. 10
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  81. 2
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  82. 133
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  83. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  84. 20
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java
  85. 40
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  86. 27
      application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java
  87. 86
      application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java
  88. 269
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  89. 257
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  90. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  91. 4
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java
  92. 12
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  93. 174
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  94. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  95. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java
  96. 16
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  97. 112
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  98. 26
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  99. 9
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  100. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

175
README.md

@ -1,43 +1,150 @@
# ThingsBoard ![banner](https://github.com/user-attachments/assets/3584b592-33dd-4fb4-91d4-47b62b34806c)
[![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1)
<div align="center">
# Open-source IoT platform for data collection, processing, visualization, and device management.
</div>
<br>
<div align="center">
💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/)&ensp;•&ensp;🌐 [Website](https://thingsboard.io/)&ensp;•&ensp;📚 [Documentation](https://thingsboard.io/docs/)&ensp;•&ensp;📔 [Blog](https://thingsboard.io/blog/)&ensp;•&ensp;▶️ [Live demo](https://demo.thingsboard.io/signup)&ensp;•&ensp;🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all)
</div>
## 🚀 Installation options
* Install ThingsBoard [On-premise](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise)
* Try [ThingsBoard Cloud](https://thingsboard.io/installations/)
* or [Use our Live demo](https://demo.thingsboard.io/signup)
## 💡 Getting started with ThingsBoard
Check out our [Getting Started guide](https://thingsboard.io/docs/getting-started-guides/helloworld/) or [watch the video](https://www.youtube.com/watch?v=80L0ubQLXsc) to learn the basics of ThingsBoard and create your first dashboard! You will learn to:
* Connect devices to ThingsBoard
* Push data from devices to ThingsBoard
* Build real-time dashboards
* Create a Customer and assign the dashboard with them.
* Define thresholds and trigger alarms
* Set up notifications via email, SMS, mobile apps, or integrate with third-party services.
## ✨ Features
<table>
<tr>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/255cca4f-b111-44e8-99ea-0af55f8e3681" alt="Provision and manage devices and assets" width="378" />
<h3>Provision and manage <br> devices and assets</h3>
</div>
<div align="center">
<p>Provision, monitor and control your IoT entities in secure way using rich server-side APIs. Define relations between your devices, assets, customers or any other entities.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/docs/user-guide/entities-and-relations/">Read more ➜</a>
</div>
<br>
</td>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/24b41d10-150a-42dd-ab1a-32ac9b5978c1" alt="Collect and visualize your data" width="378" />
<h3>Collect and visualize <br> your data</h3>
</div>
<div align="center">
<p>Collect and store telemetry data in scalable and fault-tolerant way. Visualize your data with built-in or custom widgets and flexible dashboards. Share dashboards with your customers.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/iot-data-visualization/">Read more ➜</a>
</div>
<br>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/6f2a6dd2-7b33-4d17-8b92-d1f995adda2c" alt="SCADA Dashboards" width="378" />
<h3>SCADA Dashboards</h3>
</div>
<div align="center">
<p>Monitor and control your industrial processes in real time with SCADA. Use SCADA symbols on dashboards to create and manage any workflow, offering full flexibility to design and oversee operations according to your requirements.</p>
</div>
<br>
<div align="center">
<a href="https://thingsboard.io/use-cases/scada/">Read more ➜</a>
</div>
<br>
</td>
<td width="50%" valign="top">
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/c23dcc9b-aeba-40ef-9973-49b953fc1257" alt="Process and React" width="378" />
<h3>Process and React</h3>
</div>
<div align="center">
<p>Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.<br></p>
</div>
<br>
<br>
<div align="center">
<a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/">Read more ➜</a>
</div>
<br>
</td>
</tr>
</table>
## ⚙️ Powerful IoT Rule Engine
ThingsBoard allows you to create complex [Rule Chains](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) to process data from your devices and match your application specific use cases.
[![IoT Rule Engine](https://github.com/user-attachments/assets/43d21dc9-0e18-4f1b-8f9a-b72004e12f07 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
<div align="center">
[**Read more about Rule Engine ➜**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
</div>
## 📦 Real-Time IoT Dashboards
ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that speeds up time-to-market with powerful built-in solution templates. It enables data collection and analysis from any devices, saving resources on routine tasks and letting you focus on your solution’s unique aspects. See more our Use Cases [here](https://thingsboard.io/iot-use-cases/).
[**Smart energy**](https://thingsboard.io/use-cases/smart-energy/)
[![Smart energy](https://github.com/user-attachments/assets/2a0abf13-6dc5-4f5e-9c30-1aea1d39af1e "Smart energy")](https://thingsboard.io/use-cases/smart-energy/)
[**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/)
[![SCADA Swimming pool](https://github.com/user-attachments/assets/68fd9e29-99f1-4c16-8c4c-476f4ccb20c0 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/)
[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/)
[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/)
[**Smart farming**](https://thingsboard.io/use-cases/smart-farming/)
[![Smart farming](https://github.com/user-attachments/assets/56b84c99-ef24-44e5-a903-b925b7f9d142 "Smart farming")](https://thingsboard.io/use-cases/smart-farming/)
ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. [**Smart metering**](https://thingsboard.io/smart-metering/)
<img src="./img/logo.png?raw=true" width="100" height="100">
## Documentation
ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs).
## IoT use cases
[**Smart energy**](https://thingsboard.io/smart-energy/)
[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/)
[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/)
[![SCADA Swimming pool](https://github.com/user-attachments/assets/0878a2f5-d358-47c5-b295-03b4533685cf "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/)
[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
[**Smart farming**](https://thingsboard.io/smart-farming/)
[![Smart farming](https://user-images.githubusercontent.com/8308069/152984443-a98b7d3d-ff7a-4037-9011-e71e1e6f755f.gif "Smart farming")](https://thingsboard.io/smart-farming/)
[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) [![Smart metering](https://github.com/user-attachments/assets/adc05e3d-397c-48ef-bed6-535bbd698455 "Smart metering")](https://thingsboard.io/smart-metering/)
[![IoT Rule Engine](https://img.thingsboard.io/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
[**Smart metering**](https://thingsboard.io/smart-metering/) <div align="center">
[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/)
## Getting Started [**Check more of our use cases ➜**](https://thingsboard.io/iot-use-cases/)
Collect and Visualize your IoT data in minutes by following this [guide](https://thingsboard.io/docs/getting-started-guides/helloworld/). </div>
## Support ## 🫶 Support
- [Stackoverflow](http://stackoverflow.com/questions/tagged/thingsboard) To get support, please visit our [GitHub issues page](https://github.com/thingsboard/thingsboard/issues)
## Licenses ## 📄 Licenses
This project is released under [Apache 2.0 License](./LICENSE). This project is released under [Apache 2.0 License](./LICENSE)

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

3
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -13,6 +13,7 @@
"home_page_widgets.quick_links", "home_page_widgets.quick_links",
"home_page_widgets.documentation_links", "home_page_widgets.documentation_links",
"home_page_widgets.dashboards", "home_page_widgets.dashboards",
"home_page_widgets.usage_info" "home_page_widgets.usage_info",
"api_usage"
] ]
} }

35
application/src/main/data/json/system/widget_types/api_usage.json

File diff suppressed because one or more lines are too long

4
application/src/main/data/json/system/widget_types/rpc_debug_terminal.json

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>", "templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-rpc-terminal-widget-settings", "settingsDirective": "tb-rpc-terminal-widget-settings",
@ -43,4 +43,4 @@
"public": true "public": true
} }
] ]
} }

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

@ -14,48 +14,35 @@
-- limitations under the License. -- limitations under the License.
-- --
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START -- UPDATE TENANT PROFILE CONFIGURATION START
UPDATE tenant_profile UPDATE tenant_profile
SET profile_data = jsonb_set( SET profile_data = jsonb_set(
profile_data, profile_data,
'{configuration}', '{configuration}',
( (profile_data -> 'configuration')
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration' || jsonb_strip_nulls(
|| jsonb_build_object(
COALESCE( 'minAllowedScheduledUpdateIntervalInSecForCF',
CASE CASE
WHEN profile_data -> 'configuration' -> WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN THEN NULL
jsonb_build_object( ELSE to_jsonb(3600)
'cassandraReadQueryTenantCoreRateLimits', END,
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', 'maxRelationLevelPerCfArgument',
'cassandraWriteQueryTenantCoreRateLimits', CASE
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument'
'cassandraReadQueryTenantRuleEngineRateLimits', THEN NULL
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', ELSE to_jsonb(10)
'cassandraWriteQueryTenantRuleEngineRateLimits', END
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration' )
) ),
END, false
'{}'::jsonb
)
)
) )
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration'; WHERE NOT (
(profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
AND
(profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument'
);
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END -- UPDATE TENANT PROFILE CONFIGURATION END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START
UPDATE notification_rule
SET trigger_config = REGEXP_REPLACE(
trigger_config,
'"CASSANDRA_QUERIES"',
'"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"',
'g'
)
WHERE trigger_type = 'RATE_LIMITS'
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END

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;

3
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -114,9 +114,6 @@ public class AppActor extends ContextAwareActor {
ctx.broadcastToChildrenByType(msg, EntityType.TENANT); ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
break; break;
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
//TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not.
// same for the Linked telemetry. // same for the Linked telemetry.

11
common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldLinkInitMsg.java → application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java

@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.msg.cf; package org.thingsboard.server.actors.calculatedField;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@Data @Data
public class CalculatedFieldLinkInitMsg implements ToCalculatedFieldSystemMsg { public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId; private final TenantId tenantId;
private final CalculatedFieldLink link; private final CalculatedFieldId cfId;
@Override @Override
public MsgType getMsgType() { public MsgType getMsgType() {
return MsgType.CF_LINK_INIT_MSG; return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
} }
} }

5
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java

@ -51,7 +51,7 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
@Override @Override
public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException {
log.debug("[{}] Stopping CF entity actor.", processor.tenantId); log.debug("[{}] Stopping CF entity actor.", processor.tenantId);
processor.stop(); processor.stop(false);
} }
@Override @Override
@ -75,6 +75,9 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default: default:
return false; return false;
} }

56
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -48,6 +48,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -68,7 +69,7 @@ import java.util.stream.Collectors;
*/ */
@Slf4j @Slf4j
public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor { public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor {
// (1 for result persistence + 1 for the state persistence ) // (1 for result persistence + 1 for the state persistence)
public static final int CALLBACKS_PER_CF = 2; public static final int CALLBACKS_PER_CF = 2;
final TenantId tenantId; final TenantId tenantId;
@ -91,16 +92,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
this.ctx = ctx; this.ctx = ctx;
} }
public void stop() { public void stop(boolean partitionChanged) {
log.info("[{}][{}] Stopping entity actor.", tenantId, entityId); log.info(partitionChanged ?
"[{}][{}] Stopping entity actor due to change partition event." :
"[{}][{}] Stopping entity actor.",
tenantId, entityId);
states.clear(); states.clear();
ctx.stop(ctx.getSelf()); ctx.stop(ctx.getSelf());
} }
public void process(CalculatedFieldPartitionChangeMsg msg) { public void process(CalculatedFieldPartitionChangeMsg msg) {
if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) {
log.info("[{}] Stopping entity actor due to change partition event.", entityId); stop(true);
ctx.stop(ctx.getSelf());
} }
} }
@ -224,6 +227,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} }
public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException {
log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId());
CalculatedFieldState currentState = states.get(msg.getCfId());
if (currentState == null) {
log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId());
} else {
currentState.setDirty(true);
log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId());
}
msg.getCallback().onSuccess();
}
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
@ -251,6 +266,15 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) { if (state == null) {
state = getOrInitState(ctx); state = getOrInitState(ctx);
justRestored = true; justRestored = true;
} else if (state.isDirty()) {
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId());
try {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
state.setDirty(false);
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
} }
if (state.isSizeOk()) { if (state.isSizeOk()) {
if (state.updateState(ctx, newArgValues) || justRestored) { if (state.updateState(ctx, newArgValues) || justRestored) {
@ -271,7 +295,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state != null) { if (state != null) {
return state; return state;
} else { } else {
ListenableFuture<CalculatedFieldState> stateFuture = systemContext.getCalculatedFieldProcessingService().fetchStateFromDb(ctx, entityId); ListenableFuture<CalculatedFieldState> stateFuture = cfService.fetchStateFromDb(ctx, entityId);
// Ugly but necessary. We do not expect to often fetch data from DB. Only once per <Entity, CalculatedField> pair lifetime. // Ugly but necessary. We do not expect to often fetch data from DB. Only once per <Entity, CalculatedField> pair lifetime.
// This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low.
// Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor,
@ -288,7 +312,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
boolean stateSizeChecked = false; boolean stateSizeChecked = false;
try { try {
if (ctx.isInitialized() && state.isReady()) { if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize()); state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeChecked = true; stateSizeChecked = true;
if (state.isSizeOk()) { if (state.isSizeOk()) {
@ -298,7 +322,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onSuccess(); callback.onSuccess();
} }
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.toStringOrElseNull(), null);
} }
} }
} else { } else {
@ -367,7 +391,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
return mapToArguments(ctx.getMainEntityArguments(), scope, attrDataList); return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList);
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
@ -375,17 +399,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argNames.isEmpty()) { if (argNames.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
return mapToArguments(argNames, scope, attrDataList); List<String> geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
} }
private Map<String, ArgumentEntry> mapToArguments(Map<ReferencedEntityKey, String> argNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geoArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) { for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key); String argName = argNames.get(key);
if (argName != null) { if (argName == null) {
arguments.put(argName, new SingleValueArgumentEntry(item)); continue;
}
if (geoArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue;
} }
arguments.put(argName, new SingleValueArgumentEntry(item));
} }
return arguments; return arguments;
} }

15
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java

@ -24,9 +24,6 @@ import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
/** /**
@ -70,15 +67,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg); processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg);
break; break;
case CF_INIT_PROFILE_ENTITY_MSG:
processor.onProfileEntityMsg((CalculatedFieldInitProfileEntityMsg) msg);
break;
case CF_INIT_MSG:
processor.onFieldInitMsg((CalculatedFieldInitMsg) msg);
break;
case CF_LINK_INIT_MSG:
processor.onLinkInitMsg((CalculatedFieldLinkInitMsg) msg);
break;
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg); processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg);
break; break;
@ -91,6 +79,9 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default: default:
return false; return false;
} }

240
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -35,9 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -59,7 +57,11 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@ -72,6 +74,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>(); private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>();
private final Map<CalculatedFieldId, ScheduledFuture<?>> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>();
private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldProcessingService cfExecService;
private final CalculatedFieldStateService cfStateService; private final CalculatedFieldStateService cfStateService;
@ -110,6 +113,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.clear(); calculatedFields.clear();
entityIdCalculatedFields.clear(); entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear(); entityIdCalculatedFieldLinks.clear();
cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
cfDynamicArgumentsRefreshTasks.clear();
ctx.stop(ctx.getSelf()); ctx.stop(ctx.getSelf());
} }
@ -120,37 +125,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
msg.getCallback().onSuccess(); msg.getCallback().onSuccess();
} }
public void onProfileEntityMsg(CalculatedFieldInitProfileEntityMsg msg) {
log.debug("[{}] Processing profile entity message.", msg.getTenantId().getId());
entityProfileCache.add(msg.getProfileEntityId(), msg.getEntityId());
msg.getCallback().onSuccess();
}
public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing CF init message.", msg.getCf().getId());
var cf = msg.getCf();
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
try {
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
msg.getCallback().onSuccess();
}
public void onLinkInitMsg(CalculatedFieldLinkInitMsg msg) {
log.debug("[{}] Processing CF link init message for entity [{}].", msg.getLink().getCalculatedFieldId(), msg.getLink().getEntityId());
var link = msg.getLink();
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
msg.getCallback().onSuccess();
}
public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) {
var cfId = msg.getId().cfId(); var cfId = msg.getId().cfId();
var calculatedField = calculatedFields.get(cfId); var calculatedField = calculatedFields.get(cfId);
@ -289,7 +263,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); var cfCtx = getCfCtx(cf);
try { try {
cfCtx.init(); cfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -300,11 +274,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf); addLinks(cf);
initCf(cfCtx, callback, false); scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
} }
} }
} }
private CalculatedFieldCtx getCfCtx(CalculatedField cf) {
return new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService());
}
private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException {
var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var oldCfCtx = calculatedFields.get(cfId); var oldCfCtx = calculatedFields.get(cfId);
@ -316,7 +295,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); var newCfCtx = getCfCtx(newCf);
try { try {
newCfCtx.init(); newCfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -324,6 +303,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
calculatedFields.put(newCf.getId(), newCfCtx); calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx);
if (hasSchedulingConfigChanges) {
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx);
}
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>(); List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false; boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) { for (CalculatedFieldCtx oldCtx : oldCfList) {
@ -346,7 +332,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
initCf(newCfCtx, callback, stateChanges); applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
@ -360,30 +346,20 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (cfCtx == null) { if (cfCtx == null) {
log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); log.debug("[{}] CF was already deleted [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { return;
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); }
deleteLinks(cfCtx); entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx);
EntityId entityId = cfCtx.getEntityId(); cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
EntityType entityType = cfCtx.getEntityId().getEntityType(); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
if (isProfileEntity(entityType)) { }
var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId);
if (!entityIds.isEmpty()) { private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
//TODO: no need to do this if we cache all created actors and know which one belong to us; var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
var multiCallback = new MultipleTbCallback(entityIds.size(), callback); if (existingTask != null) {
entityIds.forEach(id -> { existingTask.cancel(false);
if (isMyPartition(id, multiCallback)) { String reason = cfDeleted ? "deletion" : "update";
deleteCfForEntity(id, cfId, multiCallback); log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
}
});
} else {
callback.onSuccess();
}
} else {
if (isMyPartition(entityId, callback)) {
deleteCfForEntity(entityId, cfId, callback);
}
}
} }
} }
@ -423,31 +399,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
for (var linkProto : linksList) { for (var linkProto : linksList) {
var link = fromProto(linkProto); var link = fromProto(linkProto);
var targetEntityId = link.entityId();
var targetEntityType = targetEntityId.getEntityType();
var cf = calculatedFields.get(link.cfId()); var cf = calculatedFields.get(link.cfId());
if (EntityType.DEVICE_PROFILE.equals(targetEntityType) || EntityType.ASSET_PROFILE.equals(targetEntityType)) { withTargetEntities(link.entityId(), callback, (ids, cb) -> {
// iterate over all entities that belong to profile and push the message for corresponding CF var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb);
var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId); ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg));
if (!entityIds.isEmpty()) { });
MultipleTbCallback multipleCallback = new MultipleTbCallback(entityIds.size(), callback);
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, multipleCallback);
entityIds.forEach(entityId -> {
if (isMyPartition(entityId, multipleCallback)) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(newMsg);
}
});
} else {
callback.onSuccess();
}
} else {
if (isMyPartition(targetEntityId, callback)) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId);
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback);
getOrCreateActor(targetEntityId).tell(newMsg);
}
}
} }
} }
@ -486,26 +442,46 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result; return result;
} }
private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
EntityId entityId = cfCtx.getEntityId(); CalculatedField cf = cfCtx.getCalculatedField();
EntityType entityType = cfCtx.getEntityId().getEntityType(); if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
if (isProfileEntity(entityType)) { return;
var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); }
if (!entityIds.isEmpty()) { if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
var multiCallback = new MultipleTbCallback(entityIds.size(), callback); return;
entityIds.forEach(id -> { }
if (isMyPartition(id, multiCallback)) { if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
} return;
});
} else {
callback.onSuccess();
}
} else {
if (isMyPartition(entityId, callback)) {
initCfForEntity(entityId, cfCtx, forceStateReinit, callback);
}
} }
long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval());
var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
ScheduledFuture<?> scheduledFuture = systemContext
.schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval);
cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture);
log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId());
}
public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) {
log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId());
CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId());
if (cfCtx == null) {
log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId());
cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true);
return;
}
applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb));
}
private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback));
}
private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(msg);
} }
private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
@ -566,20 +542,38 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfs.forEach(cf -> { cfs.forEach(cf -> {
log.trace("Processing calculated field record: {}", cf); log.trace("Processing calculated field record: {}", cf);
try { try {
onFieldInitMsg(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); initCalculatedField(cf);
} catch (CalculatedFieldException e) { } catch (CalculatedFieldException e) {
log.error("Failed to process calculated field record: {}", cf, e); log.error("Failed to process calculated field record: {}", cf, e);
} }
}); });
calculatedFields.values().forEach(cf -> {
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf);
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfls.forEach(link -> { cfls.forEach(link -> {
onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)); log.trace("Processing calculated field link record: {}", link);
initCalculatedFieldLink(link);
}); });
} }
private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService());
try {
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
}
private void initEntityProfileCache() { private void initEntityProfileCache() {
PageDataIterable<ProfileEntityIdInfo> deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); PageDataIterable<ProfileEntityIdInfo> deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
for (ProfileEntityIdInfo idInfo : deviceIdInfos) { for (ProfileEntityIdInfo idInfo : deviceIdInfos) {
@ -601,4 +595,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
} }
private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx,
TbCallback callback,
BiConsumer<EntityId, TbCallback> action) {
withTargetEntities(ctx.getEntityId(), callback, (ids, cb) -> ids.forEach(id -> action.accept(id, cb)));
}
private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer<List<EntityId>, TbCallback> consumer) {
if (isProfileEntity(entityId.getEntityType())) {
var ids = entityProfileCache.getEntityIdsByProfileId(entityId);
if (ids.isEmpty()) {
parentCallback.onSuccess();
return;
}
var multiCallback = new MultipleTbCallback(ids.size(), parentCallback);
var profileEntityIds = ids.stream().filter(id -> isMyPartition(id, multiCallback)).toList();
if (profileEntityIds.isEmpty()) {
return;
}
consumer.accept(profileEntityIds, multiCallback);
return;
}
if (isMyPartition(entityId, parentCallback)) {
consumer.accept(List.of(entityId), parentCallback);
}
}
} }

13
common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java → application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java

@ -13,24 +13,25 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.msg.cf; package org.thingsboard.server.actors.calculatedField;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
@Data @Data
public class CalculatedFieldInitProfileEntityMsg implements ToCalculatedFieldSystemMsg { public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId; private final TenantId tenantId;
private final EntityId profileEntityId; private final CalculatedFieldId cfId;
private final EntityId entityId; private final TbCallback callback;
@Override @Override
public MsgType getMsgType() { public MsgType getMsgType() {
return MsgType.CF_INIT_PROFILE_ENTITY_MSG; return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
} }
} }

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java

@ -50,7 +50,7 @@ public class MultipleTbCallback implements TbCallback {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.warn("[{}][{}] onFailure.", id, callback.getId()); log.warn("[{}][{}] onFailure.", id, callback.getId(), t);
callback.onFailure(t); callback.onFailure(t);
} }
} }

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();

3
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -180,9 +180,6 @@ public class TenantActor extends RuleChainManagerActor {
onRuleChainMsg((RuleChainAwareMsg) msg); onRuleChainMsg((RuleChainAwareMsg) msg);
break; break;
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
case CF_PARTITIONS_CHANGE_MSG: case CF_PARTITIONS_CHANGE_MSG:
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true);

4
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -116,6 +116,8 @@ public class SwaggerConfiguration {
private String appVersion; private String appVersion;
@Value("${swagger.group_name:thingsboard}") @Value("${swagger.group_name:thingsboard}")
private String groupName; private String groupName;
@Value("${swagger.doc_expansion:list}")
private String docExpansion;
@Bean @Bean
public OpenAPI thingsboardApi() { public OpenAPI thingsboardApi() {
@ -172,7 +174,7 @@ public class SwaggerConfiguration {
uiProperties.setDefaultModelExpandDepth(1); uiProperties.setDefaultModelExpandDepth(1);
uiProperties.setDefaultModelRendering("example"); uiProperties.setDefaultModelRendering("example");
uiProperties.setDisplayRequestDuration(false); uiProperties.setDisplayRequestDuration(false);
uiProperties.setDocExpansion("list"); uiProperties.setDocExpansion(docExpansion);
uiProperties.setFilter("false"); uiProperties.setFilter("false");
uiProperties.setMaxDisplayedTags(null); uiProperties.setMaxDisplayedTags(null);
uiProperties.setOperationsSorter("alpha"); uiProperties.setOperationsSorter("alpha");

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

@ -26,13 +26,12 @@ 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;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
@ -40,6 +39,7 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy;
import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
@ -183,15 +183,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
@ -215,9 +212,8 @@ public class ThingsboardSecurityConfiguration {
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers http.headers(headers -> headers.defaultsDisabled()
.cacheControl(config -> {}) .crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN)))
.frameOptions(config -> {}).disable())
.cors(cors -> {}) .cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(config -> {}) .exceptionHandling(config -> {})
@ -265,4 +261,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);
} }

110
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,11 +286,9 @@ 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);
}
}
} }

35
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
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.RequestPart; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION;
@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController {
@ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/{otaPackageId}/download")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " +
OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/info/{otaPackageId}")
@ResponseBody
public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + notes = "Fetch the OTA Package object based on the provided OTA Package Id. " +
"The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/{otaPackageId}")
@ResponseBody
public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController {
"Referencing non-existing OTA Package Id will cause 'Not Found' error. " + "Referencing non-existing OTA Package Id will cause 'Not Found' error. " +
"\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH) "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage", method = RequestMethod.POST) @PostMapping(value = "/otaPackage")
@ResponseBody
public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.") public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.")
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception {
otaPackageInfo.setTenantId(getTenantId()); otaPackageInfo.setTenantId(getTenantId());
checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE); checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE);
@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController {
notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE))) requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId,
@Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'") @Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'")
@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController {
@Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"})) @Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"}))
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@Parameter(description = "OTA Package data.") @Parameter(description = "OTA Package data.")
@RequestPart MultipartFile file) throws ThingsboardException, IOException { @RequestPart MultipartFile file) throws Exception {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr);
OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId));
@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " + notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages", method = RequestMethod.GET) @GetMapping(value = "/otaPackages")
@ResponseBody
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<OtaPackageInfo> getOtaPackages(@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)
@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " + notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
@ResponseBody
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId, @PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController {
notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " +
"Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH) "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/otaPackage/{otaPackageId}")
@ResponseBody
public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);

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);
} }
} }

10
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@ -60,7 +61,10 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_
@RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) @RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX)
@Slf4j @Slf4j
public class RuleEngineController extends BaseController { public class RuleEngineController extends BaseController {
public static final int DEFAULT_TIMEOUT = 10000;
@Value("${server.rest.rule_engine.response_timeout:10000}")
public int defaultResponseTimeout;
private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. "; private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. ";
private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " + private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " +
"The generated message contains two important metadata fields:\n\n" + "The generated message contains two important metadata fields:\n\n" +
@ -85,7 +89,7 @@ public class RuleEngineController extends BaseController {
public DeferredResult<ResponseEntity> handleRuleEngineRequest( public DeferredResult<ResponseEntity> handleRuleEngineRequest(
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody);
} }
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)",
@ -104,7 +108,7 @@ public class RuleEngineController extends BaseController {
@PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = "A JSON value representing the message.", required = true) @Parameter(description = "A JSON value representing the message.", required = true)
@RequestBody String requestBody) throws ThingsboardException { @RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); return handleRuleEngineRequest(entityType, entityIdStr, null, defaultResponseTimeout, requestBody);
} }
@ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)",

2
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -162,6 +162,8 @@ public class SystemInfoController extends BaseController {
} }
systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF());
systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg());
systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF());
systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument());
systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId()));
} }
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))

133
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMethod;
@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -156,9 +148,6 @@ public class TelemetryController extends BaseController {
@Autowired @Autowired
private TbTelemetryService tbTelemetryService; private TbTelemetryService tbTelemetryService;
@Value("${transport.json.max_string_value_length:0}")
private int maxStringValueLength;
private ExecutorService executor; private ExecutorService executor;
@PostConstruct @PostConstruct
@ -314,10 +303,10 @@ public class TelemetryController extends BaseController {
@Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.") @Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.")
@RequestParam(name = "timeZone", required = false) String timeZone, @RequestParam(name = "timeZone", required = false) String timeZone,
@Parameter(description = "An integer value that represents a max number of time series data points to fetch." + @Parameter(description = "An integer value that represents a max number of time series data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
@RequestParam(name = "limit", defaultValue = "100") Integer limit, @RequestParam(name = "limit", defaultValue = "100") Integer limit,
@Parameter(description = "A string value representing the aggregation function. " + @Parameter(description = "A string value representing the aggregation function. " +
"If the interval is not specified, 'agg' parameter will use 'NONE' value.", "If the interval is not specified, 'agg' parameter will use 'NONE' value.",
schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"})) schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"}))
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr, @RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@ -337,20 +326,21 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK + @ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
@ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), @ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST),
@ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{deviceId}/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveDeviceAttributes(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true)
public DeferredResult<ResponseEntity> saveDeviceAttributes( @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -367,13 +357,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveEntityAttributesV1(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
public DeferredResult<ResponseEntity> saveEntityAttributesV1( @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)
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -390,13 +382,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveEntityAttributesV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
public DeferredResult<ResponseEntity> saveEntityAttributesV2( @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)
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -460,11 +454,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ -541,11 +535,11 @@ public class TelemetryController extends BaseController {
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ -563,11 +557,11 @@ public class TelemetryController extends BaseController {
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
@ -616,18 +610,24 @@ public class TelemetryController extends BaseController {
}); });
} }
private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException { private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, String jsonStr) throws ThingsboardException {
if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) { if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) {
return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
} }
if (json.isObject()) { JsonElement json;
List<AttributeKvEntry> attributes = extractRequestAttributes(json); try {
json = JsonParser.parseString(jsonStr);
} catch (Exception e) {
return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST);
}
if (json.isJsonObject()) {
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json);
if (attributes.isEmpty()) { if (attributes.isEmpty()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
} }
for (AttributeKvEntry attributeKvEntry : attributes) { for (AttributeKvEntry attributeKvEntry : attributes) {
if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) { if (attributeKvEntry.getKey().isBlank()) {
return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST);
} }
} }
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -885,43 +885,6 @@ public class TelemetryController extends BaseController {
return result; return result;
} }
private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
long ts = System.currentTimeMillis();
List<AttributeKvEntry> attributes = new ArrayList<>();
jsonNode.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (entry.getValue().isObject() || entry.getValue().isArray()) {
attributes.add(new BaseAttributeKvEntry(new JsonDataEntry(key, toJsonStr(value)), ts));
} else if (entry.getValue().isTextual()) {
if (maxStringValueLength > 0 && entry.getValue().textValue().length() > maxStringValueLength) {
String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", entry.getValue().textValue().length(), key, maxStringValueLength);
throw new UncheckedApiException(new InvalidParametersException(message));
}
attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
} else if (entry.getValue().isBoolean()) {
attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
} else if (entry.getValue().isDouble()) {
attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
} else if (entry.getValue().isNumber()) {
if (entry.getValue().isBigInteger()) {
throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
} else {
attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
}
}
});
return attributes;
}
private String toJsonStr(JsonNode value) {
try {
return JacksonUtil.toString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException("Can't parse jsonValue: " + value, e);
}
}
private JsonNode toJsonNode(String value) { private JsonNode toJsonNode(String value) {
try { try {
return JacksonUtil.toJsonNode(value); return JacksonUtil.toJsonNode(value);

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -116,7 +116,6 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes(); entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release // TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL. // Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData(); dataUpdateService.updateData();

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;
}
}

257
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -0,0 +1,257 @@
/**
* 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.cf;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument;
@Data
@Slf4j
public abstract class AbstractCalculatedFieldProcessingService {
protected final AttributesService attributesService;
protected final TimeseriesService timeseriesService;
protected final ApiLimitService apiLimitService;
protected final RelationService relationService;
protected ListeningExecutorService calculatedFieldCallbackExecutor;
@PostConstruct
public void init() {
calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), getExecutorNamePrefix()));
}
@PreDestroy
public void stop() {
if (calculatedFieldCallbackExecutor != null) {
calculatedFieldCallbackExecutor.shutdownNow();
}
}
protected abstract String getExecutorNamePrefix();
public ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = switch (ctx.getCalculatedField().getType()) {
case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false);
case SIMPLE, SCRIPT -> {
Map<String, ListenableFuture<ArgumentEntry>> futures = new HashMap<>();
for (var entry : ctx.getArguments().entrySet()) {
var argEntityId = resolveEntityId(entityId, entry.getValue());
var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), System.currentTimeMillis());
futures.put(entry.getKey(), argValueFuture);
}
yield futures;
}
};
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx);
result.updateState(ctx, resolveArgumentFutures(argFutures));
return result;
}, MoreExecutors.directExecutor());
}
protected EntityId resolveEntityId(EntityId entityId, Argument argument) {
return argument.getRefEntityId() != null ? argument.getRefEntityId() : entityId;
}
protected Map<String, ArgumentEntry> resolveArgumentFutures(Map<String, ListenableFuture<ArgumentEntry>> argFutures) {
return argFutures.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey, // Keep the key as is
entry -> {
try {
return entry.getValue().get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
throw new RuntimeException("Failed to fetch " + entry.getKey() + ": " + cause.getMessage(), cause);
} catch (InterruptedException e) {
throw new RuntimeException("Failed to fetch" + entry.getKey(), e);
}
}
));
}
protected Map<String, ListenableFuture<ArgumentEntry>> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
Set<Map.Entry<String, Argument>> entries = ctx.getArguments().entrySet();
if (dynamicArgumentsOnly) {
entries = entries.stream()
.filter(entry -> entry.getValue().hasDynamicSource())
.collect(Collectors.toSet());
}
for (var entry : entries) {
switch (entry.getKey()) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ->
argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), System.currentTimeMillis()));
default -> {
var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry);
argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds ->
fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor()));
}
}
}
return argFutures;
}
private ListenableFuture<List<EntityId>> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Map.Entry<String, Argument> entry) {
Argument value = entry.getValue();
if (value.getRefEntityId() != null) {
return Futures.immediateFuture(List.of(value.getRefEntityId()));
}
if (!value.hasDynamicSource()) {
return Futures.immediateFuture(List.of(entityId));
}
var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration();
return switch (refDynamicSourceConfiguration.getType()) {
case RELATION_QUERY -> {
var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
if (configuration.isSimpleRelation()) {
yield switch (configuration.getDirection()) {
case FROM ->
Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
case TO ->
Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
};
}
yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
};
}
private ListenableFuture<ArgumentEntry> fetchGeofencingKvEntry(TenantId tenantId, List<EntityId> geofencingEntities, Argument argument) {
if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) {
throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType());
}
List<ListenableFuture<Map.Entry<EntityId, AttributeKvEntry>>> kvFutures = geofencingEntities.stream()
.map(entityId -> {
var attributesFuture = attributesService.find(
tenantId,
entityId,
argument.getRefEntityKey().getScope(),
argument.getRefEntityKey().getKey()
);
return Futures.transform(attributesFuture, resultOpt ->
Map.entry(entityId, resultOpt.orElseGet(() ->
new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))),
calculatedFieldCallbackExecutor
);
}).collect(Collectors.toList());
ListenableFuture<List<Map.Entry<EntityId, AttributeKvEntry>>> allFutures = Futures.allAsList(kvFutures);
return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream()
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor());
}
protected ListenableFuture<ArgumentEntry> fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) {
return switch (argument.getRefEntityKey().getType()) {
case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs);
case ATTRIBUTE -> fetchAttribute(tenantId, entityId, argument, startTs);
case TS_LATEST -> fetchTsLatest(tenantId, entityId, argument, startTs);
};
}
private ListenableFuture<ArgumentEntry> fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument, long queryEndTs) {
long argTimeWindow = argument.getTimeWindow() == 0 ? queryEndTs : argument.getTimeWindow();
long startInterval = queryEndTs - argTimeWindow;
ReadTsKvQuery query = buildTsRollingQuery(tenantId, argument, startInterval, queryEndTs);
log.trace("[{}][{}] Fetching timeseries for query {}", tenantId, entityId, query);
ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));
return Futures.transform(tsRollingFuture, tsRolling -> {
log.debug("[{}][{}] Fetched {} timeseries for query {}", tenantId, entityId, tsRolling == null ? 0 : tsRolling.size(), query);
return ArgumentEntry.createTsRollingArgument(tsRolling, query.getLimit(), argTimeWindow);
}, calculatedFieldCallbackExecutor);
}
private ListenableFuture<ArgumentEntry> fetchAttribute(TenantId tenantId, EntityId entityId, Argument argument, long defaultLastUpdateTs) {
log.trace("[{}][{}] Fetching attribute for key {}", tenantId, entityId, argument.getRefEntityKey());
var attributeOptFuture = attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey());
return Futures.transform(attributeOptFuture, attrOpt -> {
log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt);
AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, 0L));
return transformSingleValueArgument(Optional.of(attributeKvEntry));
}, calculatedFieldCallbackExecutor);
}
protected ListenableFuture<ArgumentEntry> fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long startTs) {
String timeseriesKey = argument.getRefEntityKey().getKey();
log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey);
return transformSingleValueArgument(
Futures.transform(
timeseriesService.findLatest(tenantId, entityId, timeseriesKey),
result -> {
log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result);
return result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L)));
}, calculatedFieldCallbackExecutor));
}
private ReadTsKvQuery buildTsRollingQuery(TenantId tenantId, Argument argument, long startTs, long endTs) {
long maxDataPoints = apiLimitService.getLimit(
tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
int argumentLimit = argument.getLimit();
int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argumentLimit;
return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE);
}
}

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java

@ -34,6 +34,8 @@ public interface CalculatedFieldProcessingService {
ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments); Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);
void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List<CalculatedFieldId> cfIds, TbCallback callback); void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List<CalculatedFieldId> cfIds, TbCallback callback);

4
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java

@ -34,4 +34,8 @@ public final class CalculatedFieldResult {
(result.isTextual() && result.asText().isEmpty()); (result.isTextual() && result.asText().isEmpty());
} }
public String toStringOrElseNull() {
return result == null ? null : result.toString();
}
} }

12
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -19,11 +19,9 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -31,9 +29,8 @@ 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.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
@ -56,8 +53,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
private final CalculatedFieldService calculatedFieldService; private final CalculatedFieldService calculatedFieldService;
private final TbelInvokeService tbelInvokeService; private final TbelInvokeService tbelInvokeService;
private final ApiLimitService apiLimitService; private final ApiLimitService apiLimitService;
@Lazy private final RelationService relationService;
private final ActorSystemContext actorSystemContext;
private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>();
private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>();
@ -75,7 +71,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
cfs.forEach(cf -> { cfs.forEach(cf -> {
if (cf != null) { if (cf != null) {
calculatedFields.putIfAbsent(cf.getId(), cf); calculatedFields.putIfAbsent(cf.getId(), cf);
actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf));
} }
}); });
calculatedFields.values().forEach(cf -> { calculatedFields.values().forEach(cf -> {
@ -84,7 +79,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
cfls.forEach(link -> { cfls.forEach(link -> {
calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link);
actorSystemContext.tell(new CalculatedFieldLinkInitMsg(link.getTenantId(), link));
}); });
calculatedFieldLinks.values().stream() calculatedFieldLinks.values().stream()
.flatMap(List::stream) .flatMap(List::stream)
@ -119,7 +113,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
if (ctx == null) { if (ctx == null) {
CalculatedField calculatedField = getCalculatedField(calculatedFieldId); CalculatedField calculatedField = getCalculatedField(calculatedFieldId);
if (calculatedField != null) { if (calculatedField != null) {
ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService, relationService);
calculatedFieldsCtx.put(calculatedFieldId, ctx); calculatedFieldsCtx.put(calculatedFieldId, ctx);
log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx);
} }

174
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -15,46 +15,28 @@
*/ */
package org.thingsboard.server.service.cf; package org.thingsboard.server.service.cf;
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.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
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.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
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 org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
@ -70,20 +52,12 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@ -91,76 +65,53 @@ import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@TbRuleEngineComponent @TbRuleEngineComponent
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService {
public class DefaultCalculatedFieldProcessingService implements CalculatedFieldProcessingService {
private final AttributesService attributesService;
private final TimeseriesService timeseriesService;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final ApiLimitService apiLimitService;
private final PartitionService partitionService; private final PartitionService partitionService;
private ListeningExecutorService calculatedFieldCallbackExecutor; public DefaultCalculatedFieldProcessingService(AttributesService attributesService,
TimeseriesService timeseriesService,
@PostConstruct ApiLimitService apiLimitService,
public void init() { RelationService relationService,
calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( TbClusterService clusterService,
Math.max(4, Runtime.getRuntime().availableProcessors()), "calculated-field-callback")); PartitionService partitionService) {
super(attributesService, timeseriesService, apiLimitService, relationService);
this.clusterService = clusterService;
this.partitionService = partitionService;
} }
@PreDestroy @Override
public void stop() { protected String getExecutorNamePrefix() {
if (calculatedFieldCallbackExecutor != null) { return "calculated-field-callback";
calculatedFieldCallbackExecutor.shutdownNow();
}
} }
@Override @Override
public ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { public ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>(); return super.fetchStateFromDb(ctx, entityId);
for (var entry : ctx.getArguments().entrySet()) { }
var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId;
var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); @Override
argFutures.put(entry.getKey(), argValueFuture); public Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
// only geofencing calculated fields supports dynamic arguments scheduled updates
if (!ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) {
return Map.of();
} }
return Futures.whenAllComplete(argFutures.values()).call(() -> { return resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true));
var result = createStateByType(ctx);
result.updateState(ctx, argFutures.entrySet().stream()
.collect(Collectors.toMap(
Entry::getKey, // Keep the key as is
entry -> {
try {
// Resolve the future to get the value
return entry.getValue().get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e);
}
}
)));
return result;
}, calculatedFieldCallbackExecutor);
} }
@Override @Override
public Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments) { public Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>(); Map<String, ListenableFuture<ArgumentEntry>> argFutures = new HashMap<>();
for (var entry : arguments.entrySet()) { for (var entry : arguments.entrySet()) {
var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; if (entry.getValue().hasDynamicSource()) {
var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); continue;
}
var argEntityId = resolveEntityId(entityId, entry.getValue());
var argValueFuture = fetchArgumentValue(tenantId, argEntityId, entry.getValue(), System.currentTimeMillis());
argFutures.put(entry.getKey(), argValueFuture); argFutures.put(entry.getKey(), argValueFuture);
} }
return argFutures.entrySet().stream() return resolveArgumentFutures(argFutures);
.collect(Collectors.toMap(
Entry::getKey, // Keep the key as is
entry -> {
try {
// Resolve the future to get the value
return entry.getValue().get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e);
}
}
));
} }
@Override @Override
@ -169,7 +120,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
OutputType type = calculatedFieldResult.getType(); OutputType type = calculatedFieldResult.getType();
TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST; TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST;
TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY; TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY;
TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.getResult().toString()).build(); TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.toStringOrElseNull()).build();
clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() {
@Override @Override
public void onSuccess(TbQueueMsgMetadata metadata) { public void onSuccess(TbQueueMsgMetadata metadata) {
@ -241,69 +192,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
return builder.build(); return builder.build();
} }
private ListenableFuture<ArgumentEntry> fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) {
return switch (argument.getRefEntityKey().getType()) {
case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument);
case ATTRIBUTE -> transformSingleValueArgument(
Futures.transform(
attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()),
result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))),
calculatedFieldCallbackExecutor)
);
case TS_LATEST -> transformSingleValueArgument(
Futures.transform(
timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()),
result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))),
calculatedFieldCallbackExecutor));
};
}
private ListenableFuture<ArgumentEntry> transformSingleValueArgument(ListenableFuture<Optional<? extends KvEntry>> kvEntryFuture) {
return Futures.transform(kvEntryFuture, kvEntry -> {
if (kvEntry.isPresent() && kvEntry.get().getValue() != null) {
return ArgumentEntry.createSingleValueArgument(kvEntry.get());
} else {
return new SingleValueArgumentEntry();
}
}, calculatedFieldCallbackExecutor);
}
private ListenableFuture<ArgumentEntry> fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) {
long currentTime = System.currentTimeMillis();
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
long startTs = currentTime - timeWindow;
long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
int argumentLimit = argument.getLimit();
int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit();
ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE);
ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));
return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor);
}
private KvEntry createDefaultKvEntry(Argument argument) {
String key = argument.getRefEntityKey().getKey();
String defaultValue = argument.getDefaultValue();
if (StringUtils.isBlank(defaultValue)) {
return new StringDataEntry(key, null);
}
if (NumberUtils.isParsable(defaultValue)) {
return new DoubleDataEntry(key, Double.parseDouble(defaultValue));
}
if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) {
return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue));
}
return new StringDataEntry(key, defaultValue);
}
private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) {
return switch (ctx.getCfType()) {
case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames());
case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames());
};
}
private static class TbCallbackWrapper implements TbQueueCallback { private static class TbCallbackWrapper implements TbQueueCallback {
private final TbCallback callback; private final TbCallback callback;

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java

@ -19,10 +19,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import java.util.List; import java.util.List;
import java.util.Map;
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
@ -31,7 +34,8 @@ import java.util.List;
) )
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"), @JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"),
@JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING") @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING"),
@JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING")
}) })
public interface ArgumentEntry { public interface ArgumentEntry {
@ -58,4 +62,8 @@ public interface ArgumentEntry {
return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); return new TsRollingArgumentEntry(kvEntries, limit, timeWindow);
} }
static ArgumentEntry createGeofencingValueArgument(Map<EntityId, KvEntry> entityIdkvEntryMap) {
return new GeofencingArgumentEntry(entityIdkvEntryMap);
}
} }

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
public enum ArgumentEntryType { public enum ArgumentEntryType {
SINGLE_VALUE, TS_ROLLING SINGLE_VALUE, TS_ROLLING, GEOFENCING
} }

16
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -25,8 +25,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
public abstract class BaseCalculatedFieldState implements CalculatedFieldState { public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
@ -95,19 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
} }
} }
@Override protected void validateNewEntry(ArgumentEntry newEntry) {}
public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof TsRollingArgumentEntry) {
return;
}
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) {
throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation.");
}
}
}
protected abstract void validateNewEntry(ArgumentEntry newEntry);
private void updateLastUpdateTimestamp(ArgumentEntry entry) { private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp; long newTs = this.latestTimestamp;

112
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -25,10 +25,13 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
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.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -36,6 +39,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
@ -64,6 +68,7 @@ public class CalculatedFieldCtx {
private String expression; private String expression;
private boolean useLatestTs; private boolean useLatestTs;
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
private RelationService relationService;
private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
private ThreadLocal<Expression> customExpression; private ThreadLocal<Expression> customExpression;
@ -73,31 +78,56 @@ public class CalculatedFieldCtx {
private long maxStateSize; private long maxStateSize;
private long maxSingleValueArgumentSize; private long maxSingleValueArgumentSize;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityGeofencingArgumentNames;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
this.cfId = calculatedField.getId(); this.cfId = calculatedField.getId();
this.tenantId = calculatedField.getTenantId(); this.tenantId = calculatedField.getTenantId();
this.entityId = calculatedField.getEntityId(); this.entityId = calculatedField.getEntityId();
this.cfType = calculatedField.getType(); this.cfType = calculatedField.getType();
CalculatedFieldConfiguration configuration = calculatedField.getConfiguration(); this.arguments = new HashMap<>();
this.arguments = configuration.getArguments();
this.mainEntityArguments = new HashMap<>(); this.mainEntityArguments = new HashMap<>();
this.linkedEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>();
for (Map.Entry<String, Argument> entry : arguments.entrySet()) { this.argNames = new ArrayList<>();
var refId = entry.getValue().getRefEntityId(); this.mainEntityGeofencingArgumentNames = new ArrayList<>();
var refKey = entry.getValue().getRefEntityKey(); this.linkedEntityGeofencingArgumentNames = new ArrayList<>();
if (refId == null || refId.equals(calculatedField.getEntityId())) { this.output = calculatedField.getConfiguration().getOutput();
mainEntityArguments.put(refKey, entry.getKey()); if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) {
} else { this.arguments.putAll(argBasedConfig.getArguments());
linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); for (Map.Entry<String, Argument> entry : arguments.entrySet()) {
var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey();
if (refId == null && entry.getValue().hasDynamicSource()) {
continue;
}
if (refId == null || refId.equals(calculatedField.getEntityId())) {
mainEntityArguments.put(refKey, entry.getKey());
} else {
linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey());
}
}
this.argNames.addAll(arguments.keySet());
if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) {
this.expression = expressionBasedConfig.getExpression();
this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs();
}
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration geofencingConfig) {
geofencingConfig.getZoneGroups().forEach((zoneGroupName, config) -> {
if (config.isCfEntitySource(entityId)) {
mainEntityGeofencingArgumentNames.add(zoneGroupName);
return;
}
if (config.isLinkedCfEntitySource(entityId)) {
linkedEntityGeofencingArgumentNames.add(zoneGroupName);
}
});
} }
} }
this.argNames = new ArrayList<>(arguments.keySet());
this.output = configuration.getOutput();
this.expression = configuration.getExpression();
this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs();
this.tbelInvokeService = tbelInvokeService; this.tbelInvokeService = tbelInvokeService;
this.relationService = relationService;
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
@ -105,25 +135,29 @@ public class CalculatedFieldCtx {
} }
public void init() { public void init() {
if (CalculatedFieldType.SCRIPT.equals(cfType)) { switch (cfType) {
try { case SCRIPT -> {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); try {
initialized = true; this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
} catch (Exception e) { initialized = true;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); } catch (Exception e) {
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
}
} }
} else { case GEOFENCING -> initialized = true;
if (isValidExpression(expression)) { case SIMPLE -> {
this.customExpression = ThreadLocal.withInitial(() -> if (isValidExpression(expression)) {
new ExpressionBuilder(expression) this.customExpression = ThreadLocal.withInitial(() ->
.functions(userDefinedFunctions) new ExpressionBuilder(expression)
.implicitMultiplication(true) .functions(userDefinedFunctions)
.variables(this.arguments.keySet()) .implicitMultiplication(true)
.build() .variables(this.arguments.keySet())
); .build()
initialized = true; );
} else { initialized = true;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); } else {
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
}
} }
} }
} }
@ -293,7 +327,7 @@ public class CalculatedFieldCtx {
} }
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
boolean expressionChanged = !expression.equals(other.expression); boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
boolean outputChanged = !output.equals(other.output); boolean outputChanged = !output.equals(other.output);
return expressionChanged || outputChanged; return expressionChanged || outputChanged;
} }
@ -304,6 +338,16 @@ public class CalculatedFieldCtx {
return typeChanged || argumentsChanged; return typeChanged || argumentsChanged;
} }
public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled();
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
return refreshTriggerChanged || refreshIntervalChanged;
}
return false;
}
public String getSizeExceedsLimitMessage() { public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
} }

26
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -20,12 +20,17 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY, include = JsonTypeInfo.As.PROPERTY,
@ -34,6 +39,7 @@ import java.util.Map;
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"),
@JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"),
@JsonSubTypes.Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"),
}) })
public interface CalculatedFieldState { public interface CalculatedFieldState {
@ -44,11 +50,18 @@ public interface CalculatedFieldState {
long getLatestTimestamp(); long getLatestTimestamp();
default void setDirty(boolean dirty) {
}
default boolean isDirty() {
return false;
}
void setRequiredArguments(List<String> requiredArguments); void setRequiredArguments(List<String> requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues); boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx); ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx);
@JsonIgnore @JsonIgnore
boolean isReady(); boolean isReady();
@ -62,6 +75,15 @@ public interface CalculatedFieldState {
void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize);
void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx); default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) {
return;
}
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) {
throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation.");
}
}
}
} }

9
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java

@ -20,6 +20,7 @@ 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 com.google.common.util.concurrent.MoreExecutors;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
@ -27,6 +28,7 @@ import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList; import java.util.ArrayList;
@ -37,6 +39,7 @@ import java.util.Map;
@Data @Data
@Slf4j @Slf4j
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
public ScriptCalculatedFieldState(List<String> requiredArguments) { public ScriptCalculatedFieldState(List<String> requiredArguments) {
@ -49,11 +52,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
protected void validateNewEntry(ArgumentEntry newEntry) { public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Map<String, TbelCfArg> arguments = new LinkedHashMap<>(); Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1); List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later; args.add(new Object()); // first element is a ctx, but we will set it later;

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -20,11 +20,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
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 lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -33,6 +35,7 @@ import java.util.Map;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
public SimpleCalculatedFieldState(List<String> requiredArguments) { public SimpleCalculatedFieldState(List<String> requiredArguments) {
@ -52,7 +55,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
var expr = ctx.getCustomExpression().get(); var expr = ctx.getCustomExpression().get();
for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) { for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) {

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

Loading…
Cancel
Save