Browse Source

Merge remote-tracking branch 'upstream/master' into api-key

pull/14074/head
Andrii Landiak 10 months ago
parent
commit
5760333adb
  1. 4
      application/pom.xml
  2. 13
      application/src/main/data/json/system/widget_types/alarms_table.json
  3. 13
      application/src/main/data/json/system/widget_types/entities_table.json
  4. 15
      application/src/main/data/json/system/widget_types/markdown_html_card.json
  5. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  6. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  7. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
  8. 50
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  9. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  10. 110
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  11. 19
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  12. 16
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  13. 14
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  14. 16
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  15. 27
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  16. 16
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  17. 1
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  18. 5
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  19. 17
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  20. 40
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  22. 23
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  23. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  24. 44
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  25. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  26. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  27. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
  28. 87
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  29. 7
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  30. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  31. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  32. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  33. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  34. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  35. 130
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
  36. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
  37. 81
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
  38. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  39. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  40. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  41. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
  42. 15
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  43. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
  44. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
  45. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
  46. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  47. 2
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  48. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  49. 7
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  50. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  51. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  52. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  53. 5
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  54. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  55. 1
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  56. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  57. 6
      application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java
  58. 19
      application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
  59. 16
      application/src/main/resources/thingsboard.yml
  60. 88
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  61. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  62. 12
      application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
  63. 24
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  64. 66
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  65. 22
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  66. 81
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  67. 221
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  68. 25
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  69. 23
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  70. 196
      application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java
  71. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  72. 50
      application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
  73. 23
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  74. 3
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  75. 2
      application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
  76. 83
      application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java
  77. 125
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  78. 39
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
  79. 11
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java
  80. 25
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java
  81. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
  82. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  83. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  84. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  85. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  86. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  87. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  88. 23
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java
  89. 29
      common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java
  90. 23
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java
  91. 25
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java
  92. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  93. 6
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  94. 31
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java
  95. 3
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
  96. 23
      common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java
  97. 6
      common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
  98. 8
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java
  99. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
  100. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

4
application/pom.xml

@ -419,6 +419,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies>
<build>

13
application/src/main/data/json/system/widget_types/alarms_table.json

File diff suppressed because one or more lines are too long

13
application/src/main/data/json/system/widget_types/entities_table.json

@ -11,19 +11,13 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsDirective": "tb-entities-table-widget-settings",
"dataKeySettingsDirective": "tb-entities-table-key-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-entities-table-basic-config",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}"
},
"tags": [
"administration",
"management"
],
"resources": [
{
"link": "/api/images/system/entities_table_system_widget_image.png",
@ -36,5 +30,10 @@
"data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=",
"public": true
}
],
"scada": false,
"tags": [
"administration",
"management"
]
}

15
application/src/main/data/json/system/widget_types/markdown_html_card.json

File diff suppressed because one or more lines are too long

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

@ -27,7 +27,7 @@ SET profile_data = jsonb_set(
CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
THEN NULL
ELSE to_jsonb(3600)
ELSE to_jsonb(60)
END,
'maxRelationLevelPerCfArgument',
CASE

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

@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService;
@ -511,6 +512,10 @@ public class ActorSystemContext {
@Getter
private ResourceService resourceService;
@Autowired
@Getter
private TbResourceDataCache resourceDataCache;
@Lazy
@Autowired(required = false)
@Getter

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

@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG:
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
break;
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default:
return false;
}

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

@ -49,6 +49,7 @@ 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.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList;
import java.util.Collection;
@ -227,18 +228,6 @@ 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 {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
}
@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) {
state = getOrInitState(ctx);
justRestored = true;
} else if (state.isDirty()) {
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId());
} else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) {
log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
try {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
state.setDirty(false);
var geofencingState = (GeofencingCalculatedFieldState) state;
geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
}
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geoArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geofencingArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
Map<String, ArgumentEntry> arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argName == null) {
continue;
}
if (geoArgNames.contains(argName)) {
if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue;
}
@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argNames.isEmpty()) {
return Collections.emptyMap();
}
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys);
List<String> geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys);
}
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) {
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys);
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys);
}
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, AttributeScopeProto scope, List<String> removedAttrKeys) {
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, List<String> geofencingArgNames, AttributeScopeProto scope, List<String> removedAttrKeys) {
Map<String, ArgumentEntry> arguments = new HashMap<>();
for (String removedKey : removedAttrKeys) {
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key);
if (argName != null) {
Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
if (argName == null) {
continue;
}
if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry());
continue;
}
Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
}
return arguments;
}

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

@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG:
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
break;
case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default:
return false;
}

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

@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
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.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -57,10 +56,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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;
@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final Map<CalculatedFieldId, CalculatedFieldCtx> calculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = 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 CalculatedFieldStateService cfStateService;
@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.clear();
entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear();
cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
cfDynamicArgumentsRefreshTasks.clear();
ctx.stop(ctx.getSelf());
}
@ -274,7 +267,6 @@ 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)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
}
}
@ -300,36 +292,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
newCfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx);
if (hasSchedulingConfigChanges) {
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx);
}
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) {
if (oldCtx.getCfId().equals(newCf.getId())) {
} finally {
calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) {
if (oldCtx.getCfId().equals(newCf.getId())) {
newCfList.add(newCfCtx);
found = true;
} else {
newCfList.add(oldCtx);
}
}
if (!found) {
newCfList.add(newCfCtx);
found = true;
} else {
newCfList.add(oldCtx);
}
// 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.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
}
if (!found) {
newCfList.add(newCfCtx);
}
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
// 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)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
@ -350,19 +335,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx);
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
}
private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
if (existingTask != null) {
existingTask.cancel(false);
String reason = cfDeleted ? "deletion" : "update";
log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
}
}
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId();
log.debug("Received telemetry msg from entity [{}]", entityId);
@ -442,43 +417,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result;
}
private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
CalculatedField cf = cfCtx.getCalculatedField();
if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
return;
}
if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
return;
}
if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
return;
}
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);
@ -560,12 +498,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
} finally {
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);
}
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) {

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

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getResourceService();
}
@Override
public TbResourceDataCache getTbResourceDataCache() {
return mainCtx.getResourceDataCache();
}
@Override
public OtaPackageService getOtaPackageService() {
return mainCtx.getOtaPackageService();
@ -1054,11 +1062,20 @@ public class DefaultTbContext implements TbContext {
@Override
public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId);
if (!getTenantId().equals(actualTenantId)) {
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
}
}
@Override
public <E extends HasId<I> & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException {
TenantId actualTenantId = entity.getTenantId();
if (!getTenantId().equals(actualTenantId) && !actualTenantId.isSysTenantId()) {
throw new TbNodeException("Entity with id: '" + entity.getId() + "' specified in the configuration doesn't belong to the current or system tenant.", true);
}
}
private static String getFailureMessage(Throwable th) {
String failureMessage;
if (th != null) {

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

@ -34,6 +34,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
@ -76,6 +79,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@ -83,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -137,10 +143,16 @@ public class AssetController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody
public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception {
public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, getCurrentUser());
return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete asset (deleteAsset)",

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

@ -1745,4 +1745,18 @@ public class ControllerConstants {
MARKDOWN_CODE_BLOCK_END ;
protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities).";
public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
" If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
" UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.";
public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-7fsh4f'.";
public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " +
"By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " +
"INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " +
"For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-1.";
}

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

@ -32,6 +32,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -47,6 +50,8 @@ import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@ -54,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@ -128,10 +134,16 @@ public class CustomerController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody
public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception {
public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER);
return tbCustomerService.save(customer, getCurrentUser());
return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete Customer (deleteCustomer)",

27
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -46,8 +46,11 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -108,6 +111,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@ -117,6 +122,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -177,14 +183,21 @@ public class DeviceController extends BaseController {
@ResponseBody
public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device,
@Parameter(description = "Optional value of the device credentials to be used during device creation. " +
"If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception {
"If omitted, access token will be auto-generated.")
@RequestParam(name = "accessToken", required = false) String accessToken,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
device.setTenantId(getCurrentUser().getTenantId());
if (device.getId() != null) {
checkDeviceId(device.getId(), Operation.WRITE);
} else {
checkEntity(null, device, Resource.DEVICE);
}
return tbDeviceService.save(device, accessToken, getCurrentUser());
return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
@ -209,12 +222,18 @@ public class DeviceController extends BaseController {
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody
public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.")
@Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException {
@Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException {
Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE);
return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser());
return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete device (deleteDevice)",

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

@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -69,6 +72,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@ -76,6 +81,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/**
@ -128,7 +134,13 @@ public class EntityViewController extends BaseController {
@ResponseBody
public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView) throws Exception {
@RequestBody EntityView entityView,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null;
if (entityView.getId() == null) {
@ -137,7 +149,7 @@ public class EntityViewController extends BaseController {
} else {
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
}
return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser());
return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete entity view (deleteEntityView)",

1
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -300,6 +300,7 @@ public class ImageController extends BaseController {
tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok()
.header("Content-Type", descriptor.getMediaType())
.header("Content-Security-Policy", "default-src 'none'")
.eTag(descriptor.getEtag());
if (!cacheKey.isPublic()) {
result

5
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService;
import java.util.Map;
import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ -73,6 +76,6 @@ public class Lwm2mController extends BaseController {
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());
}
}

17
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
@ -263,6 +266,20 @@ public class TbResourceController extends BaseController {
}
}
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource", params = {"resourceIds"})
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
SecurityUser user = getCurrentUser();
List<TbResourceId> resourceIds = new ArrayList<>();
for (UUID resourceId : resourceUuids) {
resourceIds.add(new TbResourceId(resourceId));
}
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
}
@ApiOperation(value = "Get All Resource Infos (getAllResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)

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

@ -32,8 +32,10 @@ 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.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.http.HttpHeaders;
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;
@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC
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.OllamaChatModelConfig;
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 org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import static java.util.Collections.singletonMap;
@Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@ -62,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
@ -134,7 +143,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
@ -262,6 +271,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build();
}
@Override
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
var builder = OllamaChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.numCtx(chatModelConfig.contextLength())
.numPredict(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries());
var auth = chatModelConfig.providerConfig().auth();
if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) {
String credentials = basicAuth.username() + ":" + basicAuth.password();
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) {
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token()));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) {
// do nothing
} else {
throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName());
}
return builder.build();
}
private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
}

4
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
boolean check(long threshold, long warnThreshold, long value);
}
private void checkStartOfNextCycle() {
public void checkStartOfNextCycle() {
updateLock.lock();
try {
long now = System.currentTimeMillis();
myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth());
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
}

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

@ -26,7 +26,7 @@ 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.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
@ -45,6 +45,7 @@ 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 org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.HashMap;
import java.util.List;
@ -102,6 +103,10 @@ public abstract class AbstractCalculatedFieldProcessingService {
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx);
result.updateState(ctx, resolveArgumentFutures(argFutures));
// TODO: move to state.init() method after merge with alarm rules 2.0
if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) {
geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
}
return result;
}, MoreExecutors.directExecutor());
}
@ -159,19 +164,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
}
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)),
case RELATION_PATH_QUERY -> {
var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
};

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

@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) {
validateNewEntry(newEntry);
validateNewEntry(key, newEntry);
arguments.put(key, newEntry);
entryUpdated = true;
} else {
@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
}
}
protected void validateNewEntry(ArgumentEntry newEntry) {}
protected void validateNewEntry(String key, ArgumentEntry newEntry) {}
private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp;

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

@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
@ -78,9 +80,12 @@ public class CalculatedFieldCtx {
private long maxStateSize;
private long maxSingleValueArgumentSize;
private boolean relationQueryDynamicArguments;
private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityGeofencingArgumentNames;
private long scheduledUpdateIntervalMillis;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
this.calculatedField = calculatedField;
@ -101,6 +106,7 @@ public class CalculatedFieldCtx {
var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey();
if (refId == null && entry.getValue().hasDynamicSource()) {
relationQueryDynamicArguments = true;
continue;
}
if (refId == null || refId.equals(calculatedField.getEntityId())) {
@ -126,6 +132,9 @@ public class CalculatedFieldCtx {
});
}
}
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L;
}
this.tbelInvokeService = tbelInvokeService;
this.relationService = relationService;
@ -141,6 +150,7 @@ public class CalculatedFieldCtx {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
initialized = true;
} catch (Exception e) {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
}
}
@ -156,6 +166,7 @@ public class CalculatedFieldCtx {
);
initialized = true;
} else {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
}
}
@ -329,25 +340,42 @@ public class CalculatedFieldCtx {
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
boolean outputChanged = !output.equals(other.output);
return expressionChanged || outputChanged;
boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis;
return expressionChanged || outputChanged || scheduledUpdatesConfigChanged;
}
public boolean hasStateChanges(CalculatedFieldCtx other) {
boolean typeChanged = !cfType.equals(other.cfType);
boolean argumentsChanged = !arguments.equals(other.arguments);
return typeChanged || argumentsChanged;
boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other);
return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged;
}
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;
private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
}
return false;
}
public boolean hasRelationQueryDynamicArguments() {
return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1;
}
public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) {
if (!hasRelationQueryDynamicArguments()) {
return false;
}
if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) {
return false;
}
if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) {
return true;
}
return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
}

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

@ -50,13 +50,6 @@ public interface CalculatedFieldState {
long getLatestTimestamp();
default void setDirty(boolean dirty) {
}
default boolean isDirty() {
return false;
}
void setRequiredArguments(List<String> requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);

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

@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
protected void validateNewEntry(ArgumentEntry newEntry) {
protected void validateNewEntry(String key, ArgumentEntry newEntry) {
if (newEntry instanceof TsRollingArgumentEntry) {
throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields.");
throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " +
"Rolling argument entry is not supported for simple calculated fields.");
}
}

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

@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
}
public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) {
this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry)));
this(Map.of(entityId, ProtoUtils.fromProto(entry)));
}
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) {
@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
}
if (geofencingArgumentEntry.isEmpty()) {
zoneStates.clear();
return true;
}
boolean updated = false;
for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) {
if (updateZone(zoneEntry)) {
@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
zoneStates.put(zoneId, newZoneState);
return true;
}
if (newZoneState.getPerimeterDefinition() == null) {
zoneStates.remove(zoneId);
return true;
}
return existingZoneState.update(newZoneState);
}

87
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -15,16 +15,19 @@
*/
package org.thingsboard.server.service.cf.ctx.state.geofencing;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo
@Data
@Slf4j
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
private boolean dirty;
private long lastDynamicArgumentsRefreshTs = -1;
public GeofencingCalculatedFieldState() {
super(new ArrayList<>(), new HashMap<>(), false, -1);
this.dirty = false;
}
public GeofencingCalculatedFieldState(List<String> argNames) {
super(argNames);
public GeofencingCalculatedFieldState(List<String> requiredArguments) {
super(requiredArguments);
}
@Override
@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
if (arguments == null) {
arguments = new HashMap<>();
}
boolean stateUpdated = false;
for (var entry : argumentValues.entrySet()) {
String key = entry.getKey();
ArgumentEntry newEntry = entry.getValue();
checkArgumentSize(key, newEntry, ctx);
ArgumentEntry existingEntry = arguments.get(key);
boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) {
entryUpdated = switch (key) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only SINGLE_VALUE type is allowed.");
}
arguments.put(key, singleValueArgumentEntry);
yield true;
}
default -> {
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
}
arguments.put(key, geofencingArgumentEntry);
yield true;
}
};
} else {
entryUpdated = existingEntry.updateEntry(newEntry);
protected void validateNewEntry(String key, ArgumentEntry newEntry) {
switch (key) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
if (!(newEntry instanceof SingleValueArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only SINGLE_VALUE type is allowed.");
}
}
if (entryUpdated) {
stateUpdated = true;
default -> {
if (!(newEntry instanceof GeofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
}
}
}
return stateUpdated;
}
@Override
@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups();
ObjectNode resultNode = JacksonUtil.newObjectNode();
ObjectNode valuesNode = JacksonUtil.newObjectNode();
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
relationFutures.add(f);
}
});
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode);
updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
});
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode);
OutputType outputType = ctx.getOutput().getType();
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode));
if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result);
}
@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
}
private void updateResultNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
private void updateValuesNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults);
final String eventKey = argumentKey + "Event";
final String statusKey = argumentKey + "Status";
@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
}
private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) {
return valuesNode;
}
ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", latestTimestamp);
resultNode.set("values", valuesNode);
return resultNode;
}
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
boolean prevInside = zoneResults.stream()

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

@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -261,6 +263,11 @@ public class EdgeContextComponent {
@Autowired
private CalculatedFieldProcessor calculatedFieldProcessor;
@Autowired
private AiModelService aiModelService;
@Autowired
private AiModelProcessor aiModelProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) {

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

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

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

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
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.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
}
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
.setIdMSB(aiModel.getId().getId().getMostSignificantBits())
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
}
public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) {
return AiModelUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(aiModelId.getId().getMostSignificantBits())
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
}

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

@ -93,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
@TbCoreComponent
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10;
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
private final List<EdgeGrpcSession> zombieSessions = new ArrayList<>();
@Value("${edges.rpc.port}")
private int rpcPort;
@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!");
}
@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) {
try (session) {
for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
if (session.destroy()) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
}
if (!session.destroy()) {
log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.",
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session);
}
}
}
@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
private void cleanupZombieSessions() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
@ -655,9 +650,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
}
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return true;
} else {
log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return false;
}
});
} catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", e);
}
}
}

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

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -934,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
}
}
if (uplinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) {
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
}
}
} catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -139,7 +139,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};

130
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java

@ -0,0 +1,130 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.ai;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor {
@Override
public ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) {
AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (aiModelUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId);
if (aiModel.isPresent()) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get());
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
case DELETED -> {
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.AI_MODEL;
}
private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg);
Boolean wasCreated = resultPair.getFirst();
if (wasCreated) {
pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId);
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null);
}
}
private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) {
try {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModel.isPresent()) {
String aiModelAsString = JacksonUtil.toString(aiModel.get());
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData);
} else {
log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
}

21
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java → application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java

@ -13,23 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.calculatedField;
package org.thingsboard.server.service.edge.rpc.processor.ai;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
@Data
public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
public interface AiModelProcessor extends EdgeProcessor {
private final TenantId tenantId;
private final CalculatedFieldId cfId;
@Override
public MsgType getMsgType() {
return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
}
ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg);
}

81
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.ai;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.Optional;
@Slf4j
public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<AiModel> aiModelValidator;
protected Pair<Boolean, Boolean> saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) {
boolean isCreated = false;
boolean isNameUpdated = false;
try {
AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
if (aiModel == null) {
throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel");
}
Optional<AiModel> aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModelById.isEmpty()) {
aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId()));
isCreated = true;
aiModel.setId(null);
} else {
aiModel.setId(aiModelId);
}
String aiModelName = aiModel.getName();
Optional<AiModel> aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName);
if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) {
aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15);
log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}",
tenantId, aiModel.getName(), aiModelByName.get().getName());
isNameUpdated = true;
}
aiModel.setName(aiModelName);
aiModelValidator.validate(aiModel, AiModel::getTenantId);
if (isCreated) {
aiModel.setId(aiModelId);
}
edgeCtx.getAiModelService().save(aiModel, false);
} catch (Exception e) {
log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, isNameUpdated);
}
}

8
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -20,6 +20,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
@Override
public Asset save(Asset asset, User user) throws Exception {
return save(asset, NameConflictStrategy.DEFAULT, user);
}
@Override
public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = asset.getTenantId();
try {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy));
autoCommit(user, savedAsset.getId());
logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(),
actionType, user);

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.asset;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
@ -27,6 +28,8 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception;
Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
@Override
public Customer save(Customer customer, SecurityUser user) throws Exception {
return save(customer, NameConflictStrategy.DEFAULT, user);
}
@Override
public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId();
try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy));
autoCommit(user, savedCustomer.getId());
logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer;

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

@ -16,8 +16,12 @@
package org.thingsboard.server.service.entitiy.customer;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbCustomerService extends SimpleTbEntityService<Customer> {
Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception;
}

15
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override
public Device save(Device device, String accessToken, User user) throws Exception {
return save(device, accessToken, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId();
try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy));
autoCommit(user, savedDevice.getId());
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user);
@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException {
return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId();
try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials));
Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy));
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user);

5
application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
@ -33,8 +34,12 @@ public interface TbDeviceService {
Device save(Device device, String accessToken, User user) throws Exception;
Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException;
void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception {
return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user);
}
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = entityView.getTenantId();
try {
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy));
this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user);
autoCommit(user, savedEntityView.getId());
logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,

3
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener {
EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception;
EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException;
void delete(EntityView entity, User user) throws ThingsboardException;

2
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0");
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1");
private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate;

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

@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder;
}
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
}
@Override

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
ActorSystemContext actorContext,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;

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

@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId();
TbResourceId resourceId = resource.getId();
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED);
}
@Override
@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED);
}
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@ -593,6 +597,7 @@ public class DefaultTbClusterService implements TbClusterService {
EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE,
EntityType.JOB,
EntityType.TB_RESOURCE,
EntityType.USER)
|| (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)

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

@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null,
super(actorContext, null, null, null, null, null, null, null,
null, null);
this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory);

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
@ -78,13 +79,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
QueueService queueService,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

5
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -30,12 +30,14 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -72,6 +74,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
@ -202,6 +205,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));
}
eventPublisher.publishEvent(componentLifecycleMsg);

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing;
import jakarta.annotation.PostConstruct;
import org.springframework.context.ApplicationEventPublisher;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -43,12 +44,13 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbTenantProfileCache tenantProfileCache,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

1
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -102,7 +102,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
if (result.isSuccess()) {
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
}
return result;
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import jakarta.annotation.Nullable;
@ -183,7 +184,13 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
data.entrySet().stream()
.filter(dataEntry -> dataEntry.getKey().getType() == kvType &&
StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
.forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive()));
.forEach(dataEntry -> {
ParsedValue value = dataEntry.getValue();
JsonElement kvValue = (value.getDataType() == DataType.JSON)
? (JsonElement) value.getValue()
: value.toJsonPrimitive();
kvs.add(dataEntry.getKey().getKey(), kvValue);
});
return Map.entry(kvType, kvs);
})
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)

6
application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java

@ -41,9 +41,9 @@ public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache {
private final UserService userService;
@Value("${cache.userEnabled.maxSize:1000}")
@Value("${cache.userAuthDetails.maxSize:1000}")
private int cacheMaxSize;
@Value("${cache.userEnabled.timeToLiveInMinutes:30}")
@Value("${cache.userAuthDetails.timeToLiveInMinutes:30}")
private int cacheValueTtl;
private Cache<UserId, UserAuthDetails> cache;
@ -70,7 +70,7 @@ public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache {
public UserAuthDetails findUserEnabled(TenantId tenantId, UserId userId) {
lock.readLock().lock();
try {
log.trace("Retrieving user with enabled credentials status with id {} for tenant {} from cache", userId, tenantId);
log.trace("Retrieving user with enabled credentials status for id {} for tenant {} from cache", userId, tenantId);
return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id));
} finally {
lock.readLock().unlock();

19
application/src/main/java/org/thingsboard/server/utils/CsvUtils.java

@ -17,10 +17,15 @@ package org.thingsboard.server.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.input.CharSequenceReader;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -43,4 +48,18 @@ public class CsvUtils {
.collect(Collectors.toList());
}
@SneakyThrows
public static byte[] generateCsv(List<List<String>> rows) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
for (List<String> row : rows) {
csvPrinter.printRecord(row);
}
csvPrinter.flush();
}
return out.toByteArray();
}
}

16
application/src/main/resources/thingsboard.yml

@ -312,8 +312,11 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# Enable/Disable timestamp key-value partioning on read queries
# Enable/Disable timestamp key-value partitioning on read queries
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
# Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value.
# It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database
use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}"
# The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
# Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired
@ -691,9 +694,12 @@ cache:
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
userEnabled:
timeToLiveInMinutes: "${CACHE_SPECS_USER_ENABLED_TTL:120}" # User enabled cache TTL
maxSize: "${CACHE_SPECS_USER_ENABLED_MAX_SIZE:200000}" # 0 means the cache is disabled
tbResourceData:
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL
maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled
userAuthDetails:
timeToLiveInMinutes: "${CACHE_SPECS_USER_AUTH_DETAILS_TTL:120}" # User auth details cache TTL
maxSize: "${CACHE_SPECS_USER_AUTH_DETAILS_MAX_SIZE:200000}" # 0 means the cache is disabled
# Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required.
@ -1557,7 +1563,7 @@ swagger:
version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Control the initial display state of API operations and tags (none, list or full)
# Control the initial display state of API operations and tags (none or list)
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters

88
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
@ -47,10 +47,12 @@ import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.controller.CalculatedFieldControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -621,6 +623,59 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("M + 1");
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("m", argument));
config.setExpression("m + 1");
Output output = new Output();
output.setName("m1");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0);
config.setOutput(output);
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}"));
await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
config.setExpression("m m");
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}"));
await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
}
@Test
public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception {
// --- Arrange entities ---
@ -760,19 +815,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Zone groups: ATTRIBUTE on specific assets (one zone per group)
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone")));
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
@ -831,10 +880,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class);
assertThat(foundTenantProfile).isNotNull();
assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull();
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10);
int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10;
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class);
assertThat(savedTenantProfile).isNotNull();
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10);
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF);
loginTenantAdmin();
// --- Arrange entities ---
@ -869,11 +919,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY));
var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup));
@ -884,7 +931,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setOutput(out);
// Enable scheduled refresh with a 6-second interval
cfg.setScheduledUpdateInterval(6);
cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF);
cfg.setScheduledUpdateEnabled(true);
cf.setConfiguration(cfg);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);
@ -935,7 +983,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
relAllowedB.setType("AllowedZone");
doPost("/api/relation", relAllowedB).andExpect(status().isOk());
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId());
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",

12
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -155,6 +155,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.cf.CfRocksDb;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@ -1155,14 +1156,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
});
}
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) {
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) {
CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId);
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> {
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty();
log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty);
return stateDirty;
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
});
}

12
application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java

@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key-updated"))
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key-updated")
.build())
.modelId("o4-mini")
.temperature(0.2)
.topP(0.4)
@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest {
.tenantId(tenantId)
.name("Test model 1")
.configuration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("o3-pro")
.build())
.build(), AiModel.class);
@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)

24
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId);
}
@Test
public void testSaveAssetWithUniquifyStrategy() throws Exception {
Asset asset = new Asset();
asset.setName("My unique asset");
asset.setType("default");
doPost("/api/asset", asset, Asset.class);
doPost("/api/asset", asset).andExpect(status().isBadRequest());
doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest());
Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class);
assertThat(secondAsset.getName()).startsWith("My unique asset_");
Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class);
assertThat(thirdAsset.getName()).startsWith("My unique asset-");
Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1");
Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2");
}
private Asset createAsset(String name) {
Asset asset = new Asset();
asset.setName(name);

66
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -29,15 +29,23 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
@DaoSqlTest
public class CalculatedFieldControllerTest extends AbstractControllerTest {
@ -84,7 +92,35 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testSaveGeofencingCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig());
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
assertThat(savedCalculatedField).isNotNull();
assertThat(savedCalculatedField.getId()).isNotNull();
assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0);
assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@ -128,17 +164,41 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
}
private CalculatedField getCalculatedField(DeviceId deviceId) {
return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig());
}
private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(deviceId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig());
calculatedField.setConfiguration(configuration);
calculatedField.setVersion(1L);
return calculatedField;
}
private CalculatedFieldConfiguration getCalculatedFieldConfig() {
private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() {
var config = new GeofencingCalculatedFieldConfiguration();
var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea")));
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude"));
config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration));
config.setScheduledUpdateEnabled(false);
config.setOutput(output);
return config;
}
private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();

22
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -462,6 +463,27 @@ public class CustomerControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId);
}
@Test
public void testSaveCustomerWithUniquifyStrategy() throws Exception {
Customer customer = new Customer();
customer.setTitle("My unique customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest());
Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class);
assertThat(secondCustomer.getName()).startsWith("My unique customer_");
Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class);
assertThat(thirdCustomer.getName()).startsWith("My unique customer-");
Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1");
Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2");
}
private Customer createCustomer(String title) {
Customer customer = new Customer();
customer.setTitle(title);

81
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -77,10 +79,15 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.utils.CsvUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@ -1586,6 +1593,56 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(newAttributeValue, actualAttribute.get("value"));
}
@Test
public void testBulkImportDeviceWithJsonAttr() throws Exception {
String deviceName = "some_device";
String deviceType = "some_type";
String deviceAttr = "{\"threshold\":45}";
List<List<String>> content = new LinkedList<>();
content.add(Arrays.asList("NAME", "TYPE", "ATTR"));
content.add(Arrays.asList(deviceName, deviceType, deviceAttr));
byte[] bytes = CsvUtils.generateCsv(content);
BulkImportRequest request = new BulkImportRequest();
request.setFile(new String(bytes, StandardCharsets.UTF_8));
BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
name.setType(BulkImportColumnType.NAME);
BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
type.setType(BulkImportColumnType.TYPE);
BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping();
attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
attr.setKey("attr");
List<BulkImportRequest.ColumnMapping> columns = new ArrayList<>();
columns.add(name);
columns.add(type);
columns.add(attr);
mapping.setColumns(columns);
mapping.setDelimiter(',');
mapping.setUpdate(true);
mapping.setHeader(true);
request.setMapping(mapping);
BulkImportResult<Device> deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Assert.assertEquals(1, deviceBulkImportResult.getCreated().get());
Assert.assertEquals(0, deviceBulkImportResult.getErrors().get());
Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get());
Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty());
Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
Assert.assertNotNull(savedDevice);
Assert.assertEquals(deviceName, savedDevice.getName());
Assert.assertEquals(deviceType, savedDevice.getType());
Optional<AttributeKvEntry> retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get();
assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr);
assertThat(retrieved.get().getStrValue()).isNotPresent();
}
@Test
public void testSaveDeviceWithOutdatedVersion() throws Exception {
Device device = createDevice("Device v1.0");
@ -1608,6 +1665,30 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(device.getVersion()).isEqualTo(3);
}
@Test
public void testSaveDeviceWithUniquifyStrategy() throws Exception {
Device device = new Device();
device.setName("My unique device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
doPost("/api/device", device).andExpect(status().isBadRequest());
doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest());
Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class);
assertThat(secondDevice.getName()).startsWith("My unique device_");
Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class);
assertThat(thirdDevice.getName()).startsWith("My unique device-");
Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fourthDevice.getName()).isEqualTo("My unique device_1");
Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fifthDevice.getName()).isEqualTo("My unique device_2");
}
private Device createDevice(String name) {
Device device = new Device();
device.setName(name);

221
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -77,6 +77,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@ -431,14 +432,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName"));
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
List<String> retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList();
assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList());
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
@ -519,6 +523,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@Test
public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> temps = new ArrayList<>();
List<String> deviceNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
devices.add(device);
deviceNames.add(device.getName());
int temp = i * 10;
temps.add(String.valueOf(temp));
JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
.andExpect(status().isOk());
Thread.sleep(1);
}
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
String type = "device alarm" + i;
alarm.setType(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
List<EntityKey> entityFields = new ArrayList<>();
entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = new ArrayList<>();
latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
List<String> retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
}
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery();
@ -1007,6 +1072,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
countByQueryAndCheck(customerEntitiesQuery, 0);
}
@Test
public void testFindDevicesByDisplayName() throws Exception {
loginTenantAdmin();
int numOfDevices = 3;
for (int i = 0; i < numOfDevices; i++) {
Device device = new Device();
String name = "Device" + i;
device.setName(name);
device.setLabel("Device Label " + i);
device.setType("testFindDevicesByDisplayName");
Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class);
}
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceTypes(List.of("testFindDevicesByDisplayName"));
filter.setDeviceNameFilter("");
KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
// all devices with ownerName = TEST TENANT
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList());
checkEntitiesByQuery(query, numOfDevices, (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals("Device Label " + i, displayName);
});
// all devices with ownerName = TEST TENANT
EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter));
checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals("Device Label " + i, displayName);
});
}
@Test
public void testFindUsersByDisplayName() throws Exception {
loginTenantAdmin();
User userA = new User();
userA.setAuthority(Authority.TENANT_ADMIN);
userA.setFirstName("John");
userA.setLastName("Doe");
userA.setEmail("john.doe@tb.org");
userA = doPost("/api/user", userA, User.class);
var aId = userA.getId();
User userB = new User();
userB.setAuthority(Authority.TENANT_ADMIN);
userB.setFirstName("John");
userB.setEmail("john@tb.org");
userB = doPost("/api/user", userB, User.class);
var bId = userB.getId();
User userC = new User();
userC.setAuthority(Authority.TENANT_ADMIN);
userC.setLastName("Doe");
userC.setEmail("doe@tb.org");
userC = doPost("/api/user", userC, User.class);
var cId = userC.getId();
User userD = new User();
userD.setAuthority(Authority.TENANT_ADMIN);
userD.setEmail("noname@tb.org");
userD = doPost("/api/user", userD, User.class);
var dId = userD.getId();
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(EntityType.USER);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(aId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("John Doe", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(bId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("John", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(cId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("Doe", displayName);
});
query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org")));
checkEntitiesByQuery(query, 1, (i, entity) -> {
Assert.assertEquals(dId, entity.getEntityId());
String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
Assert.assertEquals("noname@tb.org", displayName);
});
}
@Test
public void testFindDevicesByOwnerNameAndOwnerType() throws Exception {
loginTenantAdmin();
@ -1044,19 +1222,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
// all devices with ownerName = TEST TENANT
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT");
BiConsumer<Integer, EntityData> checkFunction = (i, entity) -> {
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue();
String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue();
String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals(TEST_TENANT_NAME, ownerName);
Assert.assertEquals("TENANT", ownerType);
Assert.assertEquals("1" + i, alarmActiveTime);
};
checkEntitiesByQuery(query, numOfDevices, checkFunction);
// all devices with wrong ownerName
EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter));
checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null);
checkEntitiesByQuery(wrongTenantNameQuery, 0, null);
// all devices with owner type = TENANT
EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter));
checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT");
checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction);
// all devices with owner type = CUSTOMER
EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter));
checkEntitiesByQuery(customerEntitiesQuery, 0, null, null);
checkEntitiesByQuery(customerEntitiesQuery, 0, null);
}
@Test
@ -1102,6 +1291,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
findByQueryAndCheck(query, 0);
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer<Integer,EntityData> checkFunction) throws Exception {
await()
.alias("data by query")
.atMost(30, TimeUnit.SECONDS)
.until(() -> {
var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == expectedNumOfDevices;
});
if (expectedNumOfDevices == 0) {
return;
}
var data = findByQuery(query);
var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(expectedNumOfDevices, loadedEntities.size());
for (int i = 0; i < expectedNumOfDevices; i++) {
checkFunction.accept(i, loadedEntities.get(i));
}
}
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
await()
.alias("data by query")

25
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -853,4 +853,29 @@ public class EntityViewControllerTest extends AbstractControllerTest {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId);
}
@Test
public void testSaveEntityViewWithUniquifyStrategy() throws Exception {
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(tenantId);
view.setType("default");
view.setName("My unique view");
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest());
EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class);
assertThat(secondView.getName()).startsWith("My unique view_");
EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class);
assertThat(thirdView.getName()).startsWith("My unique view-");
EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fourthView.getName()).isEqualTo("My unique view_1");
EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2");
}
}

23
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues);
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() {
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
});
Assert.assertNotNull(widgetTypeInfos);
Assert.assertFalse(widgetTypeInfos.isEmpty());
Assert.assertEquals(1, widgetTypeInfos.size());
var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(dashboardInfo);
WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class);
Assert.assertNotNull(foundedWidgetType);
Assert.assertEquals(foundedWidgetType, dashboardInfo);
var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(widgetTypeInfo);
Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo);
}
@Test
@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() {
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
});
Assert.assertNull(widgetTypeInfos);
}
@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues);
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() {
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
});
Assert.assertNotNull(dashboardInfos);
Assert.assertFalse(dashboardInfos.isEmpty());
@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo);
DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals(foundDashboard, dashboardInfo);
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
}
@Test
@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() {
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
});
Assert.assertNull(dashboardInfos);
}

196
application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java

@ -0,0 +1,196 @@
/**
* 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.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AiModelEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel";
private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel";
@Test
public void testAiModel_create_update_delete() throws Exception {
// create AiModel
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId());
// update AiModel
edgeImitator.expectMessageAmount(1);
savedAiModel.setName(UPDATED_AI_MODEL_NAME);
savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName());
// delete AiModel
edgeImitator.expectMessageAmount(1);
doDelete("/api/ai/model/" + savedAiModel.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
}
@Test
public void testSendAiModelToCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
}
@Test
public void testUpdateAiModelNameOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
aiModel.setName(UPDATED_AI_MODEL_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName());
}
@Test
public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<AiModelUpdateMsg> aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class);
Assert.assertTrue(aiModelUpdateMsgOpt.isPresent());
AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get();
AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertNotEquals(savedAiModel.getUuidId(), uuid);
AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModelFromCloud);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName());
}
private AiModel createSimpleAiModel(String name) {
AiModel aiModel = new AiModel();
aiModel.setTenantId(tenantId);
aiModel.setName(name);
aiModel.setConfiguration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig(null, "test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build());
return aiModel;
}
private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder();
aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel));
aiModelUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder);
uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModel);
Assert.assertEquals(resourceTitle, aiModel.getName());
}
}

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -358,6 +359,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
}
}
if (downlinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) {
result.add(saveDownlinkMsg(aiModelUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
}

50
application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java

@ -20,9 +20,12 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoUnit.MONTHS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@DaoSqlTest
@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
private TenantId tenantId;
private Tenant savedTenant;
private TenantProfile savedTenantProfile;
private static final int MAX_ENABLE_VALUE = 5000;
private static final long VALUE_WARNING = 4500L;
@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
loginSysAdmin();
TenantProfile tenantProfile = createTenantProfile();
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant();
@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
}
@Test
public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception {
ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID()));
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setReExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTransportState(ApiUsageStateValue.ENABLED);
apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTenantId(tenantId);
apiUsageState.setEntityId(tenantId);
long now = System.currentTimeMillis();
long currentCycleTs = now - TimeUnit.DAYS.toMillis(30);
long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago
TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState);
tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs);
Map<EntityId, BaseApiUsageState> map = new HashMap<>();
map.put(tenantId, tenantApiUsageState);
Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates");
fieldToSet.setAccessible(true);
fieldToSet.set(service, map);
service.checkStartOfNextCycle();
long firstOfNextMonth = LocalDate.now()
.with((temporal) -> temporal.with(DAY_OF_MONTH, 1)
.plus(1, MONTHS))
.atStartOfDay(UTC).toInstant().toEpochMilli();
assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth);
}
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile");

23
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -257,7 +258,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo(
assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT")
.put("allowedZonesStatus", "OUTSIDE")
@ -329,7 +330,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo(
assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT")
.put("restrictedZonesEvent", "ENTERED")
@ -401,7 +402,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo(
assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesStatus", "OUTSIDE")
.put("restrictedZonesStatus", "INSIDE")
@ -453,21 +454,15 @@ public class GeofencingCalculatedFieldStateTest {
config.setEntityCoordinates(entityCoordinates);
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO);
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
allowedZonesGroup.setRelationType("CurrentZone");
allowedZonesGroup.setDirection(EntitySearchDirection.TO);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO);
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone")));
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
restrictedZonesGroup.setRelationType("CurrentZone");
restrictedZonesGroup.setDirection(EntitySearchDirection.TO);

3
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest {
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.updateState(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields.");
.hasMessage("Unsupported argument type detected for argument: key3. " +
"Rolling argument entry is not supported for simple calculated fields.");
}
@Test

2
application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java

@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest {
private static AiModelConfig constructValidOpenAiModelConfig() {
return OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)

83
application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java

@ -0,0 +1,83 @@
/**
* 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.resource;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.GeneralFileDescriptor;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@DaoSqlTest
public class DefaultResourceDataCacheTest extends AbstractControllerTest {
@MockitoSpyBean
private ResourceService resourceService;
@Autowired
private TbResourceService tbResourceService;
@MockitoSpyBean
private TbResourceDataCache resourceDataCache;
@Test
public void testGetCachedResourceData() throws Exception {
loginTenantAdmin();
TbResource resource = new TbResource();
resource.setTenantId(tenantId);
resource.setTitle("File for AI request");
resource.setResourceType(ResourceType.GENERAL);
resource.setFileName("myTestJson.json");
GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json");
resource.setDescriptorValue(descriptor);
byte[] data = "This is a test prompt for AI request.".getBytes();
resource.setData(data);
TbResourceInfo savedResource = tbResourceService.save(resource);
verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData.getData()).isEqualTo(data);
assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor);
verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId());
// retrieve resource data second time
clearInvocations(resourceService);
TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData2.getData()).isEqualTo(data);
verifyNoMoreInteractions(resourceService);
// delete resource, check cache
TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId());
tbResourceService.delete(resourceById, true, null);
verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedDataAfterDeletion).isEqualTo(null);
}
}

125
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -24,8 +25,10 @@ import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.ai.TbAiNode;
import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration;
import org.thingsboard.rule.engine.ai.TbResponseFormat;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.service.resource.TbResourceService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
private WidgetTypeService widgetTypeService;
@Autowired
private DashboardService dashboardService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private AiModelService aiModelService;
private Tenant savedTenant;
private User tenantAdmin;
@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertFalse(result.getReferences().isEmpty());
Assert.assertEquals(1, result.getReferences().size());
WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType);
EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(widgetTypeInfo);
Assert.assertNotNull(foundWidgetTypeInfo);
Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo);
Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName()));
TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource);
@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(result.getReferences());
Assert.assertEquals(1, result.getReferences().size());
DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId());
EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo);
Assert.assertNotNull(foundDashboardInfo);
Assert.assertEquals(foundDashboardInfo, dashboardInfo);
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource);
@ -598,6 +615,94 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNull(foundResource);
}
@Test
public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.GENERAL);
resource.setTitle("My resource");
resource.setFileName("test.json");
resource.setTenantId(savedTenant.getId());
resource.setData("<test></test>".getBytes());
TbResourceInfo savedResource = tbResourceService.save(resource);
RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId());
TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null);
assertThat(result).isNotNull();
assertThat(result.isSuccess()).isFalse();
assertThat(result.getReferences()).isNotEmpty().hasSize(1);
EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0);
assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test"));
TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(foundResource).isNotNull();
// force delete
TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null);
assertThat(deleteResult).isNotNull();
assertThat(deleteResult.isSuccess()).isTrue();
TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(resourceAfterDeletion).isNull();
}
private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) {
AiModel model = constructValidOpenAiModel("Test model");
AiModel saved = aiModelService.save(model);
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName("Test");
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainId ruleChainId = ruleChain.getId();
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChainId);
RuleNode aiNode = new RuleNode();
aiNode.setName("Ai request");
aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName());
aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
aiNode.setDebugSettings(DebugSettings.all());
TbAiNodeConfiguration configuration = new TbAiNodeConfiguration();
configuration.setResourceIds(Set.of(resourceId.getId()));
configuration.setModelId(saved.getId());
configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat());
configuration.setTimeoutSeconds(1);
configuration.setUserPrompt("What is temp");
aiNode.setConfiguration(JacksonUtil.valueToTree(configuration));
metaData.setNodes(Arrays.asList(aiNode));
metaData.setFirstNodeIndex(0);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
}
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
return AiModel.builder()
.tenantId(tenantId)
.name(name)
.configuration(modelConfig)
.build();
}
@Test
public void testFindTenantResourcesByTenantId() throws Exception {
loginSysAdmin();

39
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java

@ -25,10 +25,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService;
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
@ -43,6 +43,7 @@ import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any;
@ -112,6 +113,42 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt
processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2);
}
@Test
public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
String devicePayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
String payloadA = "{\"Device A\": " + devicePayload + "}";
String deviceBPayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
String payloadB = "{\"Device B\": " + deviceBPayload + "}";
testAckIsReceivedOnFailedPublishMessage("Device A", payloadA.getBytes(), "Device B", payloadB.getBytes());
}
protected void testAckIsReceivedOnFailedPublishMessage(String deviceName1, byte[] payload1, String deviceName2, byte[] payload2) throws Exception {
updateDefaultTenantProfileConfig(profileConfiguration -> {
profileConfiguration.setMaxDevices(3);
});
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload1);
// check device is created
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertNotNull(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class));
});
client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload2);
client.disconnectAndWait();
// check device was not created due to limit
doGet("/api/tenant/devices?deviceName=" + deviceName2).andExpect(status().isNotFound());
updateDefaultTenantProfileConfig(profileConfiguration -> {
profileConfiguration.setMaxDevices(0);
});
}
@Test
public void testGatewayConnect() throws Exception {
String payload = "{\"device\":\"Device A\"}";

11
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java

@ -186,4 +186,15 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract
assertFalse(callback.isPubAckReceived());
}
@Override
public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Post Telemetry device json payload")
.gatewayName("Test Post Telemetry gateway json payload")
.transportPayloadType(TransportPayloadType.JSON)
.telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC)
.build();
processBeforeTest(configProperties);
super.testAckIsReceivedOnFailedPublishMessage();
}
}

25
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java

@ -459,6 +459,31 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac
assertFalse(callback.isPubAckReceived());
}
@Override
public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Post Telemetry device proto payload")
.gatewayName("Test Post Telemetry gateway proto payload")
.transportPayloadType(TransportPayloadType.PROTOBUF)
.telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC)
.build();
processBeforeTest(configProperties);
TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder();
List<String> expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5");
String deviceName1 = "Device A";
String deviceName2 = "Device B";
TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000);
gatewayTelemetryMsgProtoBuilder.addAllMsg(List.of(deviceATelemetryMsgProto));
TransportApiProtos.GatewayTelemetryMsg payload1 = gatewayTelemetryMsgProtoBuilder.build();
TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000);
TransportApiProtos.GatewayTelemetryMsg payload2 = TransportApiProtos.GatewayTelemetryMsg.newBuilder()
.addAllMsg(List.of(deviceBTelemetryMsgProto))
.build();
super.testAckIsReceivedOnFailedPublishMessage(deviceName1, payload1.toByteArray(), deviceName2, payload2.toByteArray());
}
private DynamicSchema getDynamicSchema() {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration);

4
common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java

@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService {
AiModel save(AiModel model);
AiModel save(AiModel model, boolean doValidate);
Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService {
FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
Optional<AiModel> findAiModelByTenantIdAndName(TenantId tenantId, String name);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
@ -48,6 +49,8 @@ public interface AssetService extends EntityDaoService {
Asset saveAsset(Asset asset);
Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId);
Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -37,6 +38,8 @@ public interface CustomerService extends EntityDaoService {
Customer saveCustomer(Customer customer);
Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy);
void deleteCustomer(TenantId tenantId, CustomerId customerId);
Customer findOrCreatePublicCustomer(TenantId tenantId);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
@ -58,8 +59,12 @@ public interface DeviceService extends EntityDaoService {
Device saveDeviceWithAccessToken(Device device, String accessToken);
Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy);
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials);
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy);
Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -38,6 +39,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView);
EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy);
EntityView saveEntityView(EntityView entityView, boolean doValidate);
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType;
@ -83,6 +84,8 @@ public interface RelationService {
List<EntityRelation> findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit);
ListenableFuture<List<EntityRelation>> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
// TODO: This method may be useful for some validations in the future
// ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService {
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
ResourceExportData exportResource(TbResourceInfo resourceInfo);
List<ResourceExportData> exportResources(TenantId tenantId, Collection<TbResourceInfo> resources);
@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService {
TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data);
List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds);
}

23
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java → common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java

@ -13,25 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.calculatedField;
package org.thingsboard.server.dao.resource;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
@Data
public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
public interface TbResourceDataCache {
private final TenantId tenantId;
private final CalculatedFieldId cfId;
private final TbCallback callback;
@Override
public MsgType getMsgType() {
return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
}
FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId);
void evictResourceData(TenantId tenantId, TbResourceId resourceId);
}

29
common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java

@ -0,0 +1,29 @@
/**
* 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.common.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class GeneralFileDescriptor {
private String mediaType;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java

@ -0,0 +1,23 @@
/**
* 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.common.data;
public enum NameConflictPolicy {
FAIL,
UNIQUIFY;
}

25
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java

@ -0,0 +1,25 @@
/**
* 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.common.data;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema
public record NameConflictStrategy(NameConflictPolicy policy, String separator, UniquifyStrategy uniquifyStrategy) {
public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null, null);
}

3
common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java

@ -25,7 +25,8 @@ public enum ResourceType {
PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true),
DASHBOARD("application/json", true, true);
DASHBOARD("application/json", true, true),
GENERAL(null, false, true);
@Getter
private final String mediaType;

6
common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo {
.orElse(null);
}
@JsonIgnore
public TbResourceDataInfo toResourceDataInfo() {
return new TbResourceDataInfo(data, getDescriptor());
}
@Override
public String toString() {
return super.toString();

31
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java

@ -0,0 +1,31 @@
/**
* 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.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbResourceDataInfo {
private byte[] data;
private JsonNode descriptor;
}

3
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java

@ -17,7 +17,6 @@ package org.thingsboard.server.common.data;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.HasId;
import java.util.List;
import java.util.Map;
@ -27,6 +26,6 @@ import java.util.Map;
public class TbResourceDeleteResult {
private boolean success;
private Map<String, List<? extends HasId<?>>> references;
private Map<String, List<EntityInfo>> references;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java

@ -0,0 +1,23 @@
/**
* 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.common.data;
public enum UniquifyStrategy {
RANDOM,
INCREMENTAL;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java

@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "status",
include = JsonTypeInfo.As.PROPERTY,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true
)
@JsonSubTypes({
@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes
}
record Failure(
@Schema(
description = "A string containing details about the failure"
)
@Schema(description = "A string containing details about the failure")
String errorDetails
) implements TbChatResponse {

8
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon
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.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS")
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA")
})
public interface AiModelConfig {
@ -69,7 +72,8 @@ public interface AiModelConfig {
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS")
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA")
})
AiProviderConfig providerConfig();

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java

@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extend
permits
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {

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

Loading…
Cancel
Save