Browse Source

Merge remote-tracking branch 'upstream/master' into improvements/calculated-fields

pull/13272/head
Vladyslav_Prykhodko 1 year ago
parent
commit
9b13e733d7
  1. 63
      application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg
  2. 69
      application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg
  3. 74
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  4. 112
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  5. 75
      application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg
  6. 71
      application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg
  7. 63
      application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg
  8. 118
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  9. 119
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  10. 69
      application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg
  11. 63
      application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg
  12. 69
      application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg
  13. 115
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  14. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  15. 14
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  16. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  17. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  18. 7
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  19. 37
      application/src/main/java/org/thingsboard/server/config/mqtt/MqttClientRetransmissionSettingsComponent.java
  20. 47
      application/src/main/java/org/thingsboard/server/config/mqtt/MqttClientSettingsComponent.java
  21. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  22. 17
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  23. 11
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  24. 5
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  25. 80
      application/src/main/java/org/thingsboard/server/controller/TrendzController.java
  26. 26
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  27. 3
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  28. 3
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  29. 14
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  30. 23
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  32. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java
  33. 46
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  34. 17
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  35. 3
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java
  36. 8
      application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java
  37. 9
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  38. 2
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
  39. 2
      application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java
  40. 46
      application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java
  41. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  42. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  43. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  44. 22
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  45. 9
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  46. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
  47. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
  48. 45
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  49. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
  50. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  51. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
  52. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
  53. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java
  54. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java
  55. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java
  56. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java
  57. 20
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java
  58. 14
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
  59. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java
  60. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java
  61. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  62. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  63. 33
      application/src/main/resources/thingsboard.yml
  64. 12
      application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java
  65. 30
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  66. 2
      application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java
  67. 4
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  68. 3
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  69. 77
      application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java
  70. 163
      application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java
  71. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  72. 2
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java
  73. 2
      application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java
  74. 2
      application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java
  75. 80
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  76. 43
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  77. 228
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  78. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java
  79. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  80. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java
  81. 3
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  82. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java
  83. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  84. 14
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  85. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  86. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java
  87. 42
      application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java
  88. 63
      application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java
  89. 4
      common/cluster-api/pom.xml
  90. 4
      common/dao-api/pom.xml
  91. 29
      common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java
  92. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  93. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  94. 8
      common/data/src/main/java/org/thingsboard/server/common/data/TbProperty.java
  95. 22
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java
  96. 59
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java
  97. 3
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
  98. 1
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
  99. 6
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java
  100. 26
      common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java

63
application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Bottom right elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,125 L 100, 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M200 100H132C115 100 100 115 100 132V200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 100,200 L 100,125 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

69
application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Bottom tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

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

@ -3,11 +3,11 @@
"description": "Cross connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -406,8 +406,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -483,14 +483,6 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
@ -500,29 +492,15 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"condition": "return model.mainLine;",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
"disabled": false
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -530,11 +508,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -543,8 +521,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -552,13 +530,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -567,13 +546,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -581,7 +561,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

112
application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,10 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +202,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,13 +227,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -262,7 +242,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

75
application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left bottom elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,200 L 100,125 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -32,8 +32,8 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": "SHARED_SCOPE",
"key": "flow"
},
"getTimeSeries": {
"key": "state"
@ -44,8 +44,8 @@
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,20 +167,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -212,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -226,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -248,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M100 200L100 131C100 113.879 86.1208 100 69 100L0 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 0,100 L 75,100 Q 100,100 100,125 L 100, 200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

71
application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst leftLineReversed = \"M 97,100 H 0\";\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -487,5 +466,5 @@
}
]
}]]></tb:metadata>
<path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="bottomLine"/></g>
<path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 86 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"> </g><g tb:tag="bottomLine"> </g></g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

63
application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Left top elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 L 100,75 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M0 100H69C86.1208 100 100 86.1208 100 69V0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 0,100 L 75,100 Q 100,100 100,75 L 100, 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

118
application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg

@ -1,10 +1,9 @@
<svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg">
<tb:metadata><![CDATA[{
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{
"title": "HP Long horizontal connector",
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 400,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "arrow",
@ -13,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -54,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -73,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -108,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -167,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -206,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -214,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -227,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -249,13 +228,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -263,7 +243,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -285,5 +265,5 @@
}
]
}]]></tb:metadata>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M0 100H400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

119
application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 2,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 400';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,21 +228,20 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
"min": 0,
"step": 1
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -284,5 +263,5 @@
}
]
}]]></tb:metadata>
<path d="m100 400v-400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M 100,400 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m100 171 29 58h-58l29-58z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

69
application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Right tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst rightLine = \"M103 100H200\";\nconst rightLineReversed = \"M 200,100 H 103\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

63
application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Top right elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,75 L 100, 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,8 +53,8 @@
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
@ -130,36 +130,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -169,7 +146,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -177,11 +154,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -190,8 +167,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -199,13 +176,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -214,13 +192,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -228,7 +207,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -250,5 +229,5 @@
}
]
}]]></tb:metadata>
<path d="M100 0V69C100 86.1208 113.879 100 131 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
<path d="M 100,0 L 100,75 Q 100,100 125,100 L 200, 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

69
application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg

@ -3,11 +3,11 @@
"description": "Top tee connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}",
"tags": [
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
},
{
@ -58,8 +58,8 @@
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -174,8 +174,8 @@
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -290,8 +290,8 @@
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
@ -367,36 +367,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -406,7 +383,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -414,11 +391,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -427,8 +404,8 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
@ -436,13 +413,14 @@
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -451,13 +429,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -465,7 +444,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

115
application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg

@ -3,7 +3,7 @@
"description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n",
"tags": [
{
"tag": "arrow",
@ -12,7 +12,7 @@
},
{
"tag": "line",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}",
"stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});",
"actions": null
}
],
@ -53,18 +53,18 @@
"defaultWidgetActionSettings": null
},
{
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -72,34 +72,38 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimation",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"id": "arrowDirection",
"name": "{i18n:scada.symbol.arrow-direction}",
"hint": "{i18n:scada.symbol.arrow-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
@ -107,20 +111,16 @@
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
@ -166,36 +166,13 @@
}
],
"properties": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 6,
"required": true,
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"max": 99,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
"name": "{i18n:scada.symbol.line}",
"type": "number",
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": false,
"fieldSuffix": "px",
"min": 0,
"max": 99,
@ -205,7 +182,7 @@
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"name": "{i18n:scada.symbol.line}",
"type": "color",
"default": "#1A1A1A",
"disabled": false,
@ -213,11 +190,11 @@
},
{
"id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 4,
"subLabel": "Width",
"divider": false,
"fieldSuffix": "px",
"min": 1,
"step": 1,
@ -226,20 +203,23 @@
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line}",
"group": "{i18n:scada.symbol.flow}",
"type": "color",
"default": "#C8DFF7"
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"divider": false,
"fieldSuffix": "px",
"min": 0,
"step": 1,
@ -248,13 +228,14 @@
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"name": "{i18n:scada.symbol.flow-line-style}",
"hint": "{i18n:scada.symbol.flow-style-hint}",
"group": "{i18n:scada.symbol.flow}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"min": 0,
"step": 1,
"disabled": false,
"visible": true
@ -262,7 +243,7 @@
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"group": "{i18n:scada.symbol.flow}",
"type": "select",
"default": "butt",
"items": [
@ -284,5 +265,5 @@
}
]
}]]></tb:metadata>
<path d="M100 200L100 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
<path d="M 100,200 V 0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M100 71L129 129H71L100 71Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

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

@ -32,6 +32,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService;
@ -639,6 +640,10 @@ public class ActorSystemContext {
@Getter
private long cfCalculationResultTimeout;
@Autowired
@Getter
private MqttClientSettings mqttClientSettings;
@Getter
@Setter
private TbActorSystem actorSystem;

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

@ -23,14 +23,15 @@ import org.bouncycastle.util.Arrays;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
@ -234,7 +235,8 @@ public class DefaultTbContext implements TbContext {
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(getTenantId().getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg)).build();
.setTbMsgProto(TbMsg.toProto(tbMsg))
.build();
mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, callback);
}
@ -313,7 +315,7 @@ public class DefaultTbContext implements TbContext {
TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(getTenantId().getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.setTbMsgProto(TbMsg.toProto(tbMsg))
.addAllRelationTypes(relationTypes);
if (failureMessage != null) {
msg.setFailureMessage(failureMessage);
@ -1010,13 +1012,17 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getAuditLogService();
}
@Override
public MqttClientSettings getMqttClientSettings() {
return mainCtx.getMqttClientSettings();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
return metaData;
}
@Override
public void schedule(Runnable runnable, long delay, TimeUnit timeUnit) {
mainCtx.getScheduler().schedule(runnable, delay, timeUnit);

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -381,7 +381,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
ToRuleEngineMsg toQueueMsg = ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(newMsg))
.setTbMsgProto(TbMsg.toProto(newMsg))
.build();
clusterService.pushMsgToRuleEngine(tpi, newMsg.getId(), toQueueMsg, callbackWrapper);
}

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -210,7 +210,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
TransportProtos.ToRuleEngineMsg toQueueMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.setTbMsgProto(TbMsg.toProto(tbMsg))
.build();
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source);

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

@ -47,6 +47,7 @@ import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.AuthExceptionHandler;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -129,6 +130,9 @@ public class ThingsboardSecurityConfiguration {
@Autowired
private RateLimitProcessingFilter rateLimitProcessingFilter;
@Autowired
private AuthExceptionHandler authExceptionHandler;
@Bean
protected PayloadSizeFilter payloadSizeFilter() {
return new PayloadSizeFilter(maxPayloadSizeConfig);
@ -235,7 +239,8 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authExceptionHandler, buildRestLoginProcessingFilter().getClass());
if (oauth2Configuration != null) {
http.oauth2Login(login -> login
.authorizationEndpoint(config -> config

37
application/src/main/java/org/thingsboard/server/config/mqtt/MqttClientRetransmissionSettingsComponent.java

@ -0,0 +1,37 @@
/**
* 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.config.mqtt;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "mqtt.client.retransmission")
public class MqttClientRetransmissionSettingsComponent {
@PositiveOrZero
private int maxAttempts;
@PositiveOrZero
private long initialDelayMillis;
@PositiveOrZero
private double jitterFactor;
}

47
application/src/main/java/org/thingsboard/server/config/mqtt/MqttClientSettingsComponent.java

@ -0,0 +1,47 @@
/**
* 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.config.mqtt;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.context.annotation.Configuration;
import org.thingsboard.rule.engine.api.MqttClientSettings;
@ToString
@EqualsAndHashCode
@Configuration
@RequiredArgsConstructor
public class MqttClientSettingsComponent implements MqttClientSettings {
private final MqttClientRetransmissionSettingsComponent retransmissionSettingsComponent;
@Override
public int getRetransmissionMaxAttempts() {
return retransmissionSettingsComponent.getMaxAttempts();
}
@Override
public long getRetransmissionInitialDelayMillis() {
return retransmissionSettingsComponent.getInitialDelayMillis();
}
@Override
public double getRetransmissionJitterFactor() {
return retransmissionSettingsComponent.getJitterFactor();
}
}

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

@ -157,7 +157,7 @@ public class AlarmController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE)
@ResponseBody
public Boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.DELETE);

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

@ -437,14 +437,7 @@ public abstract class BaseController {
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {
if (!logControllerErrorStackTrace) { // not to log the error twice
log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception));
}
if (cause instanceof ConstraintViolationException) {
return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE);
} else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
}
@ -964,8 +957,12 @@ public abstract class BaseController {
}
}
protected CalculatedField checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException {
return checkEntityId(calculatedFieldId, calculatedFieldService::findById, operation);
private void checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException {
validateId(calculatedFieldId, "Invalid entity id");
SecurityUser user = getCurrentUser();
CalculatedField cf = calculatedFieldService.findById(user.getTenantId(), calculatedFieldId);
checkNotNull(cf, calculatedFieldId.getEntityType().getNormalName() + " with id [" + calculatedFieldId + "] is not found");
checkEntityId(cf.getEntityId(), operation);
}
protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) {

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

@ -61,7 +61,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngi
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Collections;
@ -138,7 +137,6 @@ public class CalculatedFieldController extends BaseController {
public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.")
@RequestBody CalculatedField calculatedField) throws Exception {
calculatedField.setTenantId(getTenantId());
checkEntity(calculatedField.getId(), calculatedField, Resource.CALCULATED_FIELD);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser());
return tbCalculatedFieldService.save(calculatedField, getCurrentUser());
@ -188,7 +186,7 @@ public class CalculatedFieldController extends BaseController {
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.DELETE);
CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
tbCalculatedFieldService.delete(calculatedField, getCurrentUser());
}
@ -202,7 +200,7 @@ public class CalculatedFieldController extends BaseController {
public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.READ);
CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
TenantId tenantId = getCurrentUser().getTenantId();
return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1))
@ -287,7 +285,10 @@ public class CalculatedFieldController extends BaseController {
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {
case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}

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

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -87,6 +88,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private DebugModeRateLimitsConfig debugModeRateLimitsConfig;
@Autowired
private TrendzSettingsService trendzSettingsService;
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@ -158,6 +162,7 @@ public class SystemInfoController extends BaseController {
}
systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF());
systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg());
systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId()));
}
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)

80
application/src/main/java/org/thingsboard/server/controller/TrendzController.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
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 static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api")
public class TrendzController extends BaseController {
private final TrendzSettingsService trendzSettingsService;
@ApiOperation(value = "Save Trendz settings (saveTrendzSettings)",
notes = "Saves Trendz settings for this tenant.\n" + NEW_LINE +
"Here is an example of the Trendz settings:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"enabled\": true,\n" +
" \"baseUrl\": \"https://some.domain.com:18888/also_necessary_prefix\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/trendz/settings")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public TrendzSettings saveTrendzSettings(@RequestBody TrendzSettings trendzSettings,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.WRITE);
TenantId tenantId = user.getTenantId();
trendzSettingsService.saveTrendzSettings(tenantId, trendzSettings);
return trendzSettings;
}
@ApiOperation(value = "Get Trendz Settings (getTrendzSettings)",
notes = "Retrieves Trendz settings for this tenant." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@GetMapping("/trendz/settings")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
public TrendzSettings getTrendzSettings(@AuthenticationPrincipal SecurityUser user) {
TenantId tenantId = user.getTenantId();
return trendzSettingsService.findTrendzSettings(tenantId);
}
}

26
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -21,7 +21,9 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
@ -139,6 +141,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
ThingsboardException thingsboardException = (ThingsboardException) exception;
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) {
handleSubscriptionException((ThingsboardException) exception, response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) {
handleDatabaseException(thingsboardException.getCause(), response);
} else {
handleThingsboardException((ThingsboardException) exception, response);
}
@ -148,8 +152,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleAccessDeniedException(response);
} else if (exception instanceof AuthenticationException) {
handleAuthenticationException((AuthenticationException) exception, response);
} else if (exception instanceof MaxPayloadSizeExceededException) {
} else if (exception instanceof MaxPayloadSizeExceededException) {
handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception);
} else if (exception instanceof DataAccessException e) {
handleDatabaseException(e, response);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
@ -201,6 +207,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class));
}
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
ThingsboardErrorResponse errorResponse;
if (databaseException instanceof ConstraintViolationException) {
errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
} else {
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);
}
writeResponse(errorResponse, response);
}
private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
@ -233,4 +250,11 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
}
}
// TODO: refactor this class to use this method instead of boilerplate JacksonUtil.writeValue(response.getWriter(), ...
private void writeResponse(ThingsboardErrorResponse errorResponse, HttpServletResponse response) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(errorResponse.getStatus());
JacksonUtil.writeValue(response.getWriter(), errorResponse);
}
}

3
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -68,6 +68,7 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.eclipse.leshan.core.LwM2m.Version.V1_0;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE;
@Service
@TbCoreComponent
@ -258,7 +259,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), SINGLE));
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();

3
application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java

@ -284,7 +284,8 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator());
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg))
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTbMsgProto(TbMsg.toProto(tbMsg))
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);

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

@ -17,10 +17,8 @@ package org.thingsboard.server.service.entitiy;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.thingsboard.server.cluster.TbClusterService;
@ -31,16 +29,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -55,12 +47,6 @@ public abstract class AbstractTbEntityService {
@Autowired
private Environment env;
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@Autowired
protected DbCallbackExecutorService dbExecutor;
@Autowired(required = false)
protected TbLogEntityActionService logEntityActionService;
@Autowired(required = false)

23
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -190,11 +190,24 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
}
@Override
public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId();
logEntityActionService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(),
ActionType.ALARM_DELETE, user, alarm.getId());
return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId());
public boolean delete(Alarm alarm, User user) {
var tenantId = alarm.getTenantId();
var alarmId = alarm.getId();
var alarmOriginator = alarm.getOriginator();
boolean deleted;
try {
deleted = alarmSubscriptionService.deleteAlarm(tenantId, alarmId);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(alarmOriginator.getEntityType()), ActionType.ALARM_DELETE, user, e, alarmId);
throw e;
}
if (deleted) {
logEntityActionService.logEntityAction(tenantId, alarmOriginator, alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, alarmId);
}
return deleted;
}
private static long getOrDefault(long ts) {

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

@ -43,5 +43,6 @@ public interface TbAlarmService {
void unassignDeletedUserAlarms(TenantId tenantId, UserId userId, String userTitle, List<UUID> alarms, long unassignTs);
Boolean delete(Alarm alarm, User user);
boolean delete(Alarm alarm, User user);
}

2
application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java

@ -86,7 +86,7 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen
otaPackage.setContentType(contentType);
otaPackage.setData(ByteBuffer.wrap(data));
otaPackage.setDataSize((long) data.length);
OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage);
OtaPackageInfo savedOtaPackage = new OtaPackageInfo(otaPackageService.saveOtaPackage(otaPackage));
logEntityActionService.logEntityAction(tenantId, savedOtaPackage.getId(), savedOtaPackage, null, actionType, user);
return savedOtaPackage;
} catch (Exception e) {

46
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -25,21 +25,30 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE;
@Service
@Profile("install")
@Slf4j
@ -51,19 +60,50 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private RuleChainService ruleChainService;
@Autowired
private RelationService relationService;
@Autowired
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
JpaExecutorService jpaExecutorService;
private DbUpgradeExecutorService executorService;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
//TODO: should be cleaned after each release
updateInputNodes();
log.info("Data updated.");
}
private void updateInputNodes() {
log.info("Creating relations for input nodes...");
int n = 0;
var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024);
for (RuleNode inputNode : inputNodes) {
try {
RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId"))
.filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id)))
.orElse(null);
if (targetRuleChainId == null) {
continue;
}
EntityRelation relation = new EntityRelation();
relation.setFrom(inputNode.getRuleChainId());
relation.setTo(targetRuleChainId);
relation.setType(EntityRelation.USES_TYPE);
relation.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(TenantId.SYS_TENANT_ID, relation);
n++;
} catch (Exception e) {
log.error("Failed to save relation for input node: {}", inputNode, e);
}
}
log.info("Created {} relations for input nodes", n);
}
@Override
public void upgradeRuleNodes() {
int totalRuleNodesUpgraded = 0;
@ -107,7 +147,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
ruleNodeId, ruleNodeType, fromVersion, toVersion);
try {
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(ruleNode, ruleNodeClassInfo);
saveFutures.add(jpaExecutorService.submit(() -> {
saveFutures.add(executorService.submit(() -> {
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, fromVersion, toVersion);

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

@ -552,19 +552,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
proto.getScope(), KvProtoUtil.toAttributeKvList(proto.getDataList()), callback);
} else if (msg.hasAttrDelete()) {
TbAttributeDeleteProto proto = msg.getAttrDelete();
if (proto.hasNotifyDevice()) {
// handles old messages with deprecated 'notifyDevice'
subscriptionManagerService.onAttributesDelete(
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback);
} else {
// handles new messages without 'notifyDevice'
subscriptionManagerService.onAttributesDelete(
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), callback);
}
subscriptionManagerService.onAttributesDelete(
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), callback);
} else if (msg.hasTsDelete()) {
TbTimeSeriesDeleteProto proto = msg.getTsDelete();
subscriptionManagerService.onTimeSeriesDelete(

3
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.msg.gen.MsgProtos;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.UUID;
@ -34,7 +35,7 @@ public class SequentialByOriginatorIdTbRuleEngineSubmitStrategy extends Sequenti
@Override
protected EntityId getEntityId(TransportProtos.ToRuleEngineMsg msg) {
try {
MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(msg.getTbMsg());
MsgProtos.TbMsgProto proto = ProtoUtils.getTbMsgProto(msg);
return EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
} catch (InvalidProtocolBufferException e) {
log.warn("[{}] Failed to parse TbMsg: {}", queueName, msg);

8
application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java

@ -19,8 +19,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -125,7 +125,7 @@ public class TbRuleEngineProcessingStrategyFactory {
}
log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size());
if (log.isTraceEnabled()) {
toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY)));
toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY)));
}
if (pauseBetweenRetries > 0) {
try {
@ -164,10 +164,10 @@ public class TbRuleEngineProcessingStrategyFactory {
log.debug("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size());
}
if (log.isTraceEnabled()) {
result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY)));
result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY)));
}
if (log.isTraceEnabled()) {
result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY)));
result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY)));
}
return new TbRuleEngineProcessingDecision(true, null);
}

9
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -178,7 +179,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
new TbMsgPackCallback(id, tenantId, packCtx, stats.getTimer(tenantId, SUCCESSFUL_STATUS), stats.getTimer(tenantId, FAILED_STATUS)) :
new TbMsgPackCallback(id, tenantId, packCtx);
try {
if (!toRuleEngineMsg.getTbMsg().isEmpty()) {
if (!toRuleEngineMsg.getTbMsg().isEmpty() || toRuleEngineMsg.hasTbMsgProto()) {
forwardToRuleEngineActor(config.getName(), tenantId, toRuleEngineMsg, callback);
} else {
callback.onSuccess();
@ -189,7 +190,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
}
private void forwardToRuleEngineActor(String queueName, TenantId tenantId, ToRuleEngineMsg toRuleEngineMsg, TbMsgCallback callback) {
TbMsg tbMsg = TbMsg.fromBytes(queueName, toRuleEngineMsg.getTbMsg().toByteArray(), callback);
TbMsg tbMsg = ProtoUtils.fromTbMsgProto(queueName, toRuleEngineMsg, callback);
QueueToRuleEngineMsg msg;
ProtocolStringList relationTypesList = toRuleEngineMsg.getRelationTypesList();
Set<String> relationTypes;
@ -207,7 +208,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
log.info("[{}] {} to process [{}] messages", queueKey, prefix, map.size());
for (Map.Entry<UUID, TbProtoQueueMsg<ToRuleEngineMsg>> pending : map.entrySet()) {
ToRuleEngineMsg tmp = pending.getValue().getValue();
TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY);
TbMsg tmpMsg = ProtoUtils.fromTbMsgProto(config.getName(), tmp, TbMsgCallback.EMPTY);
RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey());
if (printAll) {
log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo);
@ -236,7 +237,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<T
}
for (TbProtoQueueMsg<ToRuleEngineMsg> msg : msgs) {
try {
MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray());
MsgProtos.TbMsgProto tbMsgProto = ProtoUtils.getTbMsgProto(msg.getValue());
EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB()));
TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator);

2
application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java

@ -133,7 +133,7 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
TransportProtos.RestApiCallResponseMsgProto msg = TransportProtos.RestApiCallResponseMsgProto.newBuilder()
.setRequestIdMSB(requestId.getMostSignificantBits())
.setRequestIdLSB(requestId.getLeastSignificantBits())
.setResponse(TbMsg.toByteString(tbMsg))
.setResponseProto(TbMsg.toProto(tbMsg))
.build();
clusterService.pushNotificationToCore(serviceId, msg, null);
}

2
application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java

@ -73,7 +73,7 @@ public class DefaultRuleEngineCallService implements RuleEngineCallService {
UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB());
Consumer<TbMsg> consumer = requests.remove(requestId);
if (consumer != null) {
consumer.accept(TbMsg.fromBytes(null, restApiCallResponseMsg.getResponse().toByteArray(), TbMsgCallback.EMPTY));
consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), restApiCallResponseMsg.getResponse(), TbMsgCallback.EMPTY));
} else {
log.trace("[{}] Unknown or stale rest api call response received", requestId);
}

46
application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java

@ -0,0 +1,46 @@
/**
* 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.security.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthExceptionHandler extends OncePerRequestFilter {
private final ThingsboardErrorResponseHandler errorResponseHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
try {
filterChain.doFilter(request, response);
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
errorResponseHandler.handle(e, response);
}
}
}

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

@ -50,8 +50,7 @@ public enum Resource {
VERSION_CONTROL,
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS,
CALCULATED_FIELD(EntityType.CALCULATED_FIELD);
MOBILE_APP_SETTINGS;
private final Set<EntityType> entityTypes;

2
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value="sysAdminPermissions")
@Component(value = "sysAdminPermissions")
public class SysAdminPermissions extends AbstractPermissions {
public SysAdminPermissions() {

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

@ -55,7 +55,6 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ));
put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

22
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -20,10 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -36,7 +33,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
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.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -73,7 +69,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
private final TbServiceInfoProvider serviceInfoProvider;
private final TbQueueProducerProvider producerProvider;
private final TbLocalSubscriptionService localSubscriptionService;
private final TbClusterService clusterService;
private final SubscriptionSchedulerComponent scheduler;
private final Lock subsLock = new ReentrantLock();
@ -204,15 +199,14 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
@Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, TbCallback callback) {
onAttributesDelete(tenantId, entityId, scope, keys, false, callback);
}
@Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback callback) {
processAttributesUpdate(entityId, scope,
keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList()));
if (entityId.getEntityType() == EntityType.DEVICE && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), scope, keys), null);
try {
List<AttributeKvEntry> deletedEntries = keys.stream()
.<AttributeKvEntry>map(key -> new BaseAttributeKvEntry(0L, new StringDataEntry(key, "")))
.toList();
processAttributesUpdate(entityId, scope, deletedEntries);
} catch (Exception e) {
callback.onFailure(e);
return;
}
callback.onSuccess();
}

9
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java

@ -41,15 +41,6 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, TbCallback empty);
/**
* This method is retained solely for backwards compatibility, specifically to handle
* legacy proto messages that include the notifyDevice field.
*
* @deprecated as of 4.0, this method will be removed in future releases.
*/
@Deprecated(forRemoval = true, since = "4.0")
void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback empty);
void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback);
void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java

@ -46,7 +46,7 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
}
@Override
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) {
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider, CompareResult compareResult) {
Asset savedAsset = assetService.saveAsset(asset);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedAsset, exportData, idProvider);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java

@ -49,7 +49,7 @@ public class AssetProfileImportService extends BaseEntityImportService<AssetProf
}
@Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);

45
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.api.client.util.Objects;
import com.google.common.util.concurrent.FutureCallback;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -117,10 +119,10 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
E prepared = prepare(ctx, entity, existingEntity, exportData, idProvider);
boolean saveOrUpdate = existingEntity == null || compare(ctx, exportData, prepared, existingEntity);
CompareResult compareResult = compare(ctx, exportData, prepared, existingEntity);
if (saveOrUpdate) {
E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider);
if (compareResult.isUpdateNeeded()) {
E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider, compareResult);
boolean created = existingEntity == null;
importResult.setCreated(created);
importResult.setUpdated(!created);
@ -137,6 +139,17 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
return importResult;
}
@Data
@AllArgsConstructor
static class CompareResult {
private boolean updateNeeded;
private boolean externalIdChangedOnly;
public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded;
}
}
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
return importCalculatedFields(ctx, prepared, exportData, idProvider);
}
@ -148,18 +161,30 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
protected abstract E prepare(EntitiesImportCtx ctx, E entity, E oldEntity, D exportData, IdProvider idProvider);
protected boolean compare(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
protected CompareResult compare(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
if (existing == null) {
log.debug("[{}] Found new entity.", prepared.getId());
return new CompareResult(true);
}
var newCopy = deepCopy(prepared);
var existingCopy = deepCopy(existing);
cleanupForComparison(newCopy);
cleanupForComparison(existingCopy);
var result = !newCopy.equals(existingCopy);
if (result) {
var updateNeeded = isUpdateNeeded(ctx, exportData, newCopy, existingCopy);
boolean externalIdChangedOnly = false;
if (updateNeeded) {
log.debug("[{}] Found update.", prepared.getId());
log.debug("[{}] From: {}", prepared.getId(), newCopy);
log.debug("[{}] To: {}", prepared.getId(), existingCopy);
cleanupExternalId(newCopy);
cleanupExternalId(existingCopy);
externalIdChangedOnly = newCopy.equals(existingCopy);
}
return result;
return new CompareResult(updateNeeded, externalIdChangedOnly);
}
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
return !prepared.equals(existing);
}
protected abstract E deepCopy(E e);
@ -172,7 +197,11 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
}
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider);
protected void cleanupExternalId(E e) {
e.setExternalId(null);
}
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult);
protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java

@ -52,7 +52,7 @@ public class CustomerImportService extends BaseEntityImportService<CustomerId, C
}
@Override
protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider) {
protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider, CompareResult compareResult) {
if (!customer.isPublic()) {
return customerService.saveCustomer(customer);
} else {

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -75,7 +75,7 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
}
@Override
protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData<Dashboard> exportData, IdProvider idProvider) {
protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData<Dashboard> exportData, IdProvider idProvider, CompareResult compareResult) {
var tenantId = ctx.getTenantId();
Set<ShortCustomerInfo> assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream()
@ -116,8 +116,8 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
}
@Override
protected boolean compare(EntitiesImportCtx ctx, EntityExportData<Dashboard> exportData, Dashboard prepared, Dashboard existing) {
return super.compare(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration());
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, EntityExportData<Dashboard> exportData, Dashboard prepared, Dashboard existing) {
return super.isUpdateNeeded(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration());
}
@Override

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java

@ -63,7 +63,7 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
}
@Override
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider, CompareResult compareResult) {
Device savedDevice;
if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
exportData.getCredentials().setId(null);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java

@ -51,7 +51,7 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
}
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java

@ -55,7 +55,7 @@ public class EntityViewImportService extends BaseEntityImportService<EntityViewI
}
@Override
protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData<EntityView> exportData, IdProvider idProvider) {
protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData<EntityView> exportData, IdProvider idProvider, CompareResult compareResult) {
return entityViewService.saveEntityView(entityView);
}

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java

@ -135,7 +135,7 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
}
@Override
protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData<NotificationRule> exportData, IdProvider idProvider) {
protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData<NotificationRule> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationRule);
return notificationRuleService.saveNotificationRule(ctx.getTenantId(), notificationRule);
}

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java

@ -80,7 +80,7 @@ public class NotificationTargetImportService extends BaseEntityImportService<Not
}
@Override
protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData<NotificationTarget> exportData, IdProvider idProvider) {
protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData<NotificationTarget> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationTarget);
return notificationTargetService.saveNotificationTarget(ctx.getTenantId(), notificationTarget);
}

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java

@ -48,7 +48,7 @@ public class NotificationTemplateImportService extends BaseEntityImportService<N
}
@Override
protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData<NotificationTemplate> exportData, IdProvider idProvider) {
protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData<NotificationTemplate> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationTemplate);
return notificationTemplateService.saveNotificationTemplate(ctx.getTenantId(), notificationTemplate);
}

20
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java

@ -58,21 +58,29 @@ public class ResourceImportService extends BaseEntityImportService<TbResourceId,
}
@Override
protected boolean compare(EntitiesImportCtx ctx, EntityExportData<TbResource> exportData, TbResource prepared, TbResource existing) {
return true;
protected TbResource deepCopy(TbResource resource) {
return new TbResource(resource);
}
@Override
protected TbResource deepCopy(TbResource resource) {
return new TbResource(resource);
protected void cleanupForComparison(TbResource resource) {
super.cleanupForComparison(resource);
resource.setSearchText(null);
if (resource.getDescriptor().isNull()) {
resource.setDescriptor(null);
}
}
@Override
protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData<TbResource> exportData, IdProvider idProvider) {
protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData<TbResource> exportData, IdProvider idProvider, CompareResult compareResult) {
if (resource.getResourceType() == ResourceType.IMAGE) {
return new TbResource(imageService.saveImage(resource));
} else {
resource = resourceService.saveResource(resource);
if (compareResult.isExternalIdChangedOnly()) {
resource = resourceService.saveResource(resource, false);
} else {
resource = resourceService.saveResource(resource);
}
resource.setData(null);
resource.setPreview(null);
return resource;

14
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeDao;
@ -103,7 +105,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
}
@Override
protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider) {
protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider, CompareResult compareResult) {
ruleChain = ruleChainService.saveRuleChain(ruleChain);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
exportData.getMetaData().setRuleChainId(ruleChain.getId());
@ -115,15 +117,15 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
}
@Override
protected boolean compare(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
boolean different = super.compare(ctx, exportData, prepared, existing);
if (!different) {
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
boolean updateNeeded = super.isUpdateNeeded(ctx, exportData, prepared, existing);
if (!updateNeeded) {
RuleChainMetaData newMD = exportData.getMetaData();
RuleChainMetaData existingMD = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), prepared.getId());
existingMD.setRuleChainId(null);
different = !newMD.equals(existingMD);
updateNeeded = !newMD.equals(existingMD);
}
return different;
return updateNeeded;
}
@Override

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java

@ -44,13 +44,13 @@ public class WidgetTypeImportService extends BaseEntityImportService<WidgetTypeI
}
@Override
protected WidgetTypeDetails saveOrUpdate(EntitiesImportCtx ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData, IdProvider idProvider) {
protected WidgetTypeDetails saveOrUpdate(EntitiesImportCtx ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData, IdProvider idProvider, CompareResult compareResult) {
return widgetTypeService.saveWidgetType(widgetsBundle);
}
@Override
protected boolean compare(EntitiesImportCtx ctx, WidgetTypeExportData exportData, WidgetTypeDetails prepared, WidgetTypeDetails existing) {
return true;
protected CompareResult compare(EntitiesImportCtx ctx, WidgetTypeExportData exportData, WidgetTypeDetails prepared, WidgetTypeDetails existing) {
return new CompareResult(true);
}
@Override

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java

@ -49,7 +49,7 @@ public class WidgetsBundleImportService extends BaseEntityImportService<WidgetsB
}
@Override
protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider) {
protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (CollectionsUtil.isNotEmpty(exportData.getWidgets())) {
exportData.getWidgets().forEach(widgetTypeNode -> {
String bundleAlias = widgetTypeNode.remove("bundleAlias").asText();
@ -75,8 +75,8 @@ public class WidgetsBundleImportService extends BaseEntityImportService<WidgetsB
}
@Override
protected boolean compare(EntitiesImportCtx ctx, WidgetsBundleExportData exportData, WidgetsBundle prepared, WidgetsBundle existing) {
return true;
protected CompareResult compare(EntitiesImportCtx ctx, WidgetsBundleExportData exportData, WidgetsBundle prepared, WidgetsBundle existing) {
return new CompareResult(true);
}
@Override

2
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -454,7 +454,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
private VersionLoadResult onError(EntityId externalId, Throwable e) {
return analyze(e, externalId).orElse(VersionLoadResult.error(EntityLoadError.runtimeError(e)));
return analyze(e, externalId).orElse(VersionLoadResult.error(EntityLoadError.runtimeError(e, externalId)));
}
private Optional<VersionLoadResult> analyze(Throwable e, EntityId externalId) {

2
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -115,7 +115,7 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}
@Override
public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
public boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId);
onAlarmDeleted(result);
return result.isSuccessful();

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

@ -644,6 +644,9 @@ cache:
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
trendzSettings:
timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL
maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:
@ -1628,6 +1631,12 @@ queue:
- key: max.poll.records
# Max poll records for edqs.state topic
value: "${TB_QUEUE_KAFKA_EDQS_STATE_MAX_POLL_RECORDS:512}"
# If you override any default Kafka topic name using environment variables, you must also specify the related consumer properties
# for the new topic in `consumer-properties-per-topic-inline`. Otherwise, the topic will not inherit its expected configuration (e.g., max.poll.records, timeouts, etc).
# Each entry sets a single property for a specific topic. To define multiple properties for a topic, repeat the topic key.
# Format: "topic1:key=value;topic1:key=value;topic2:key=value"
# Example: tb_core_updated:max.poll.records=10;tb_core_updated:bootstrap.servers=kafka1:9092,kafka2:9092;tb_edge_updated:auto.offset.reset=latest
consumer-properties-per-topic-inline: "${TB_QUEUE_KAFKA_CONSUMER_PROPERTIES_PER_TOPIC_INLINE:}"
other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000"
other: # DEPRECATED. In this section, you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside
# - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
@ -1952,3 +1961,27 @@ mobileApp:
googlePlayLink: "${TB_MOBILE_APP_GOOGLE_PLAY_LINK:https://play.google.com/store/apps/details?id=org.thingsboard.demo.app}"
# Link to App Store for Thingsboard Live mobile application
appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}"
mqtt:
# MQTT client configuration parameters
client:
# Parameters that control the retransmission mechanism.
# This mechanism only applies to the handling of MQTT Publish, Subscribe, Unsubscribe and Pubrel messages.
# With the updated default settings:
# - After sending the message, wait approximately 5000 ms (± jitter) for the 1st attempt.
# - The 2nd attempt will occur after roughly 5000 * 2 = 10,000 ms (± jitter).
# - The 3rd attempt will occur after roughly 5000 * 4 = 20,000 ms (± jitter).
# - The 4th "attempt" will not actually perform a retransmission.
# Instead, the system will detect that the maximum number of attempts has been reached and drop the pending message.
retransmission:
# Maximum number of retransmission attempts allowed.
# If the attempt count exceeds this value, retransmissions will stop and the pending message will be dropped.
max_attempts: "${TB_MQTT_CLIENT_RETRANSMISSION_MAX_ATTEMPTS:3}"
# Base delay (in milliseconds) before the first retransmission attempt, measured from the moment the message is sent.
# Subsequent delays are calculated using exponential backoff.
# This base delay is also used as the reference value for applying jitter.
initial_delay_millis: "${TB_MQTT_CLIENT_RETRANSMISSION_INITIAL_DELAY_MILLIS:5000}"
# Jitter factor applied to the calculated retransmission delay.
# The actual delay is randomized within a range defined by multiplying the base delay by a factor between (1 - jitter_factor) and (1 + jitter_factor).
# For example, a jitter_factor of 0.15 means the actual delay may vary by up to ±15% of the base delay.
jitter_factor: "${TB_MQTT_CLIENT_RETRANSMISSION_JITTER_FACTOR:0.15}"

12
application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java

@ -770,11 +770,11 @@ class DefaultTbContextTest {
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.ignoringFields("tbMsgProto_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.setTbMsgProto(TbMsg.toProto(expectedTbMsg))
.addAllRelationTypes(List.of(connectionType)).build());
var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue();
@ -827,11 +827,11 @@ class DefaultTbContextTest {
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.ignoringFields("tbMsgProto_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.setTbMsgProto(TbMsg.toProto(expectedTbMsg))
.build());
var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue();
@ -1009,11 +1009,11 @@ class DefaultTbContextTest {
ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue();
assertThat(actualToRuleEngineMsg).usingRecursiveComparison()
.ignoringFields("tbMsg_")
.ignoringFields("tbMsgProto_.id_")
.isEqualTo(ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits())
.setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(expectedTbMsg))
.setTbMsgProto(TbMsg.toProto(expectedTbMsg))
.setFailureMessage(EXCEPTION_MSG)
.addAllRelationTypes(List.of(TbNodeConnectionType.FAILURE)).build());

30
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
@ -57,6 +58,8 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@ -308,6 +311,21 @@ public class AlarmControllerTest extends AbstractControllerTest {
testNotifyEntityNever(alarm.getId(), alarm);
}
@Test
public void testDeleteNonExistentAlarm() throws Exception {
loginTenantAdmin();
var nonExistentAlarmId = Uuids.timeBased();
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/alarm/" + nonExistentAlarmId)
.andExpect(status().isNotFound())
.andExpect(statusReason(is("Alarm with id [" + nonExistentAlarmId + "] is not found")));
verifyNoInteractions(tbClusterService, auditLogService);
}
@Test
public void testClearAlarmViaCustomer() throws Exception {
loginCustomerUser();
@ -634,12 +652,12 @@ public class AlarmControllerTest extends AbstractControllerTest {
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
AlarmInfo alarmInfo = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class);
Assert.assertNotNull(alarmInfo);
Assert.assertNull(alarmInfo.getAssigneeId());
Assert.assertTrue(alarmInfo.getAssignTs() >= afterAssignmentTs);
});
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
AlarmInfo alarmInfo = doGet("/api/alarm/info/" + alarmId.getId(), AlarmInfo.class);
Assert.assertNotNull(alarmInfo);
Assert.assertNull(alarmInfo.getAssigneeId());
Assert.assertTrue(alarmInfo.getAssignTs() >= afterAssignmentTs);
});
}
@Test

2
application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java

@ -216,7 +216,7 @@ public class OtaPackageControllerTest extends AbstractControllerTest {
Assert.assertEquals(CHECKSUM_ALGORITHM, savedFirmware.getChecksumAlgorithm().name());
Assert.assertEquals(CHECKSUM, savedFirmware.getChecksum());
testNotifyEntityAllOneTime(savedFirmware, savedFirmware.getId(), savedFirmware.getId(),
testNotifyEntityAllOneTime(new OtaPackageInfo(savedFirmware), savedFirmware.getId(), savedFirmware.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.UPDATED);
}

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

File diff suppressed because one or more lines are too long

3
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -762,7 +762,8 @@ public class TenantControllerTest extends AbstractControllerTest {
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg)).build();
.setTbMsgProto(TbMsg.toProto(tbMsg))
.build();
tbClusterService.pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, null);
return tbMsg;
}

77
application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class TrendzControllerTest extends AbstractControllerTest {
private final String trendzUrl = "https://some.domain.com:18888/also_necessary_prefix";
@Before
public void setUp() throws Exception {
loginTenantAdmin();
TrendzSettings trendzSettings = new TrendzSettings();
trendzSettings.setEnabled(true);
trendzSettings.setBaseUrl(trendzUrl);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
}
@Test
public void testTrendzSettingsWhenTenant() throws Exception {
loginTenantAdmin();
TrendzSettings trendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(trendzSettings).isNotNull();
assertThat(trendzSettings.isEnabled()).isTrue();
assertThat(trendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
String updatedUrl = "https://some.domain.com:18888/tenant_trendz";
trendzSettings.setBaseUrl(updatedUrl);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
TrendzSettings updatedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(updatedTrendzSettings).isEqualTo(trendzSettings);
}
@Test
public void testTrendzSettingsWhenCustomer() throws Exception {
loginCustomerUser();
TrendzSettings newTrendzSettings = new TrendzSettings();
newTrendzSettings.setEnabled(true);
newTrendzSettings.setBaseUrl("https://some.domain.com:18888/customer_trendz");
doPost("/api/trendz/settings", newTrendzSettings).andExpect(status().isForbidden());
TrendzSettings fetchedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(fetchedTrendzSettings).isNotNull();
assertThat(fetchedTrendzSettings.isEnabled()).isTrue();
assertThat(fetchedTrendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
}
}

163
application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java

@ -15,15 +15,12 @@
*/
package org.thingsboard.server.service.entitiy.alarm;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.User;
@ -35,6 +32,9 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
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.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.alarm.AlarmService;
@ -47,7 +47,6 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -55,58 +54,57 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = DefaultTbAlarmService.class)
@TestPropertySource(properties = {
"server.log_controller_error_stack_trace=false"
})
public class DefaultTbAlarmServiceTest {
@SpringJUnitConfig(DefaultTbAlarmService.class)
class DefaultTbAlarmServiceTest {
@MockBean
protected DbCallbackExecutorService dbExecutor;
TbLogEntityActionService logEntityActionService;
@MockBean
protected TbLogEntityActionService logEntityActionService;
EdgeService edgeService;
@MockBean
protected EdgeService edgeService;
AlarmService alarmService;
@MockBean
protected AlarmService alarmService;
TbAlarmCommentService alarmCommentService;
@MockBean
protected TbAlarmCommentService alarmCommentService;
AlarmSubscriptionService alarmSubscriptionService;
@MockBean
protected AlarmSubscriptionService alarmSubscriptionService;
CustomerService customerService;
@MockBean
protected CustomerService customerService;
TbClusterService tbClusterService;
@MockBean
protected TbClusterService tbClusterService;
EntitiesVersionControlService vcService;
@MockBean
private EntitiesVersionControlService vcService;
AccessControlService accessControlService;
@MockBean
private AccessControlService accessControlService;
TenantService tenantService;
@MockBean
private TenantService tenantService;
AssetService assetService;
@MockBean
private AssetService assetService;
DeviceService deviceService;
@MockBean
private DeviceService deviceService;
AssetProfileService assetProfileService;
@MockBean
private AssetProfileService assetProfileService;
DeviceProfileService deviceProfileService;
@MockBean
private DeviceProfileService deviceProfileService;
@MockBean
private EntityService entityService;
@SpyBean
EntityService entityService;
@Autowired
DefaultTbAlarmService service;
TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
CustomerId customerId = new CustomerId(Uuids.timeBased());
@Test
public void testSave() throws ThingsboardException {
void testSave() throws ThingsboardException {
var alarm = new AlarmInfo();
when(alarmSubscriptionService.createAlarm(any())).thenReturn(AlarmApiCallResult.builder()
.successful(true)
@ -115,45 +113,99 @@ public class DefaultTbAlarmServiceTest {
.build());
service.save(alarm, new User());
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ADDED), any());
verify(alarmSubscriptionService, times(1)).createAlarm(any());
verify(logEntityActionService).logEntityAction(any(), any(), any(), any(), eq(ActionType.ADDED), any());
verify(alarmSubscriptionService).createAlarm(any());
}
@Test
public void testAck() throws ThingsboardException {
void testAck() throws ThingsboardException {
var alarm = new Alarm();
when(alarmSubscriptionService.acknowledgeAlarm(any(), any(), anyLong()))
.thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).alarm(new AlarmInfo()).build());
service.ack(alarm, new User(new UserId(UUID.randomUUID())));
verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any());
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_ACK), any());
verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong());
verify(alarmCommentService).saveAlarmComment(any(), any(), any());
verify(logEntityActionService).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_ACK), any());
verify(alarmSubscriptionService).acknowledgeAlarm(any(), any(), anyLong());
}
@Test
public void testClear() throws ThingsboardException {
void testClear() throws ThingsboardException {
var alarm = new Alarm();
alarm.setAcknowledged(true);
when(alarmSubscriptionService.clearAlarm(any(), any(), anyLong(), any()))
.thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).alarm(new AlarmInfo()).build());
service.clear(alarm, new User(new UserId(UUID.randomUUID())));
verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any());
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_CLEAR), any());
verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any());
verify(alarmCommentService).saveAlarmComment(any(), any(), any());
verify(logEntityActionService).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_CLEAR), any());
verify(alarmSubscriptionService).clearAlarm(any(), any(), anyLong(), any());
}
@Test
public void testDelete() {
service.delete(new Alarm(), new User());
void testDelete_deleteApiReturnsTrue_shouldLogActionAndReturnTrue() {
// GIVEN
var alarmOriginator = new DeviceId(Uuids.timeBased());
var alarm = new Alarm(new AlarmId(Uuids.timeBased()));
alarm.setTenantId(tenantId);
alarm.setCustomerId(customerId);
alarm.setOriginator(alarmOriginator);
var user = new User();
when(alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId())).thenReturn(true);
verify(logEntityActionService, times(1)).logEntityAction(any(), any(), any(), any(), eq(ActionType.ALARM_DELETE), any(), any());
verify(alarmSubscriptionService, times(1)).deleteAlarm(any(), any());
// WHEN
boolean actual = service.delete(alarm, user);
assertThat(actual).isTrue();
verify(logEntityActionService).logEntityAction(tenantId, alarmOriginator, alarm, alarm.getCustomerId(), ActionType.ALARM_DELETE, user, alarm.getId());
verify(alarmSubscriptionService).deleteAlarm(tenantId, alarm.getId());
}
@Test
public void testUnassignAlarm() throws ThingsboardException {
void testDelete_deleteApiReturnsFalse_shouldNotLogActionAndReturnFalse() {
// GIVEN
var alarm = new Alarm(new AlarmId(Uuids.timeBased()));
alarm.setTenantId(tenantId);
var user = new User();
// WHEN
boolean actual = service.delete(alarm, user);
assertThat(actual).isFalse();
verifyNoInteractions(logEntityActionService);
verify(alarmSubscriptionService).deleteAlarm(tenantId, alarm.getId());
}
@Test
void testDelete_deleteApiThrowsException_shouldLogFailedActionAndRethrow() {
// GIVEN
var alarmOriginator = new DeviceId(Uuids.timeBased());
var alarm = new Alarm(new AlarmId(Uuids.timeBased()));
alarm.setTenantId(tenantId);
alarm.setOriginator(alarmOriginator);
var user = new User();
var exception = new RuntimeException("failed to delete alarm");
when(alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId())).thenThrow(exception);
// WHEN-THEN
assertThatThrownBy(() -> service.delete(alarm, user))
.isInstanceOf(RuntimeException.class)
.hasMessage("failed to delete alarm");
verify(logEntityActionService).logEntityAction(tenantId, new DeviceId(EntityId.NULL_UUID), ActionType.ALARM_DELETE, user, exception, alarm.getId());
verify(alarmSubscriptionService).deleteAlarm(tenantId, alarm.getId());
}
@Test
void testUnassignAlarm() throws ThingsboardException {
AlarmInfo alarm = new AlarmInfo();
alarm.setId(new AlarmId(UUID.randomUUID()));
when(alarmSubscriptionService.unassignAlarm(any(), any(), anyLong()))
@ -174,12 +226,11 @@ public class DefaultTbAlarmServiceTest {
.comment(commentNode)
.build();
verify(alarmCommentService, times(1))
.saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(user));
verify(alarmCommentService).saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(user));
}
@Test
public void testUnassignDeletedUserAlarms() throws ThingsboardException {
void testUnassignDeletedUserAlarms() throws ThingsboardException {
AlarmInfo alarm = new AlarmInfo();
alarm.setId(new AlarmId(UUID.randomUUID()));
@ -189,7 +240,7 @@ public class DefaultTbAlarmServiceTest {
User user = new User();
user.setEmail("testEmail@gmail.com");
user.setId(new UserId(UUID.randomUUID()));
service.unassignDeletedUserAlarms(new TenantId(UUID.randomUUID()), user.getId(), user.getTitle(), List.of(alarm.getUuidId()), System.currentTimeMillis());
service.unassignDeletedUserAlarms(tenantId, user.getId(), user.getTitle(), List.of(alarm.getUuidId()), System.currentTimeMillis());
ObjectNode commentNode = JacksonUtil.newObjectNode();
commentNode.put("subtype", "ASSIGN");
@ -200,9 +251,7 @@ public class DefaultTbAlarmServiceTest {
.comment(commentNode)
.build();
verify(alarmCommentService, times(1))
.saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(null));
verify(alarmCommentService).saveAlarmComment(eq(alarm), eq(expectedAlarmComment), eq(null));
}
}

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -737,7 +737,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
.setTenantIdMSB(tenantId.getMostSignificantBits())
.setTenantIdLSB(tenantId.getLeastSignificantBits())
.addRelationTypes("Success")
.setTbMsg(TbMsg.toByteString(tbMsg))
.setTbMsgProto(TbMsg.toProto(tbMsg))
.build());
}

2
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java

@ -263,7 +263,7 @@ public class TbRuleEngineStrategyTest {
.setTenantIdMSB(tenantId.getMostSignificantBits())
.setTenantIdLSB(tenantId.getLeastSignificantBits())
.addRelationTypes("Success")
.setTbMsg(TbMsg.toByteString(tbMsg))
.setTbMsgProto(TbMsg.toProto(tbMsg))
.build());
}

2
application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java

@ -55,7 +55,7 @@ class DefaultTbRuleEngineRpcServiceTest {
var restApiCallResponseMsgProto = TransportProtos.RestApiCallResponseMsgProto.newBuilder()
.setRequestIdMSB(requestId.getMostSignificantBits())
.setRequestIdLSB(requestId.getLeastSignificantBits())
.setResponse(TbMsg.toByteString(msg))
.setResponseProto(TbMsg.toProto(msg))
.build();
// WHEN

2
application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java

@ -144,7 +144,7 @@ public class DefaultRuleEngineCallServiceTest {
private TransportProtos.RestApiCallResponseMsgProto getResponse(UUID requestId, TbMsg msg) {
return TransportProtos.RestApiCallResponseMsgProto.newBuilder()
.setResponse(TbMsg.toByteString(msg))
.setResponseProto(TbMsg.toProto(msg))
.setRequestIdMSB(requestId.getMostSignificantBits())
.setRequestIdLSB(requestId.getLeastSignificantBits())
.build();

80
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java

@ -33,6 +33,9 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
@ -2345,6 +2348,83 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(expected, actual);
}
@Test
public void isMap_Test() throws ExecutionException, InterruptedException {
msgStr = """
{}
""";
decoderStr = """
return isMap(msg);
""";
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertTrue((Boolean) actual);
decoderStr = """
return isList(msg);
""";
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
}
@Test
public void isList_Test() throws ExecutionException, InterruptedException {
msgStr = """
{}
""";
decoderStr = String.format("""
var list = [];
list.add(0x35);
return isList(list);
""");
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertTrue((Boolean) actual);
decoderStr = String.format("""
var list = [];
list.add(0x35);
return isMap(list);
""");
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
decoderStr = String.format("""
var list = [];
list.add(0x35);
return isArray(list);
""");
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
}
@Test
public void isArray_Test() throws ExecutionException, InterruptedException {
msgStr = """
{}
""";
decoderStr = """
var array = new int[3];
array[0] = 1;
array[1] = 2;
array[2] = 3;
return isArray(array);
""";
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertTrue((Boolean) actual);
decoderStr = """
var array = new int[3];
array[0] = 1;
array[1] = 2;
array[2] = 3;
return isList(array);
""";
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
}
private List splice(List oldList, int start, int deleteCount, Object... values) {
start = initStartIndex(oldList, start);
deleteCount = deleteCount < 0 ? 0 : Math.min(deleteCount, (oldList.size() - start));

43
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -40,6 +40,9 @@ import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
@ -113,6 +116,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME;
@DaoSqlTest
public class VersionControlTest extends AbstractControllerTest {
@ -648,6 +653,26 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
@Test
public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource");
String versionId = createVersion("resources", EntityType.TB_RESOURCE);
TbResource resource = findResource(resourceInfo.getName());
loadVersion(versionId, EntityType.TB_RESOURCE);
TbResource importedResource = findResource(resource.getName());
checkImportedEntity(tenantId1, resource, tenantId1, importedResource);
checkImportedResourceData(resource, importedResource);
}
protected void checkImportedResourceData(TbResource resource, TbResource importedResource) {
assertThat(importedResource.getName()).isEqualTo(resource.getName());
assertThat(importedResource.getData()).isEqualTo(resource.getData());
assertThat(importedResource.getResourceKey()).isEqualTo(resource.getResourceKey());
assertThat(importedResource.getResourceType()).isEqualTo(resource.getResourceType());
}
private <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) {
assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1);
assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2);
@ -1128,4 +1153,22 @@ public class VersionControlTest extends AbstractControllerTest {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData();
}
private TbResourceInfo createResource(String name) {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle(name);
resource.setFileName(JS_TEST_FILE_NAME);
resource.setEncodedData(TEST_DATA);
return saveTbResource(resource);
}
private TbResourceInfo saveTbResource(TbResource tbResource) {
return doPost("/api/resource", tbResource, TbResourceInfo.class);
}
private TbResource findResource(String name) throws Exception {
return doGetTypedWithPageLink("/api/resource?", new TypeReference<PageData<TbResource>>() {}, new PageLink(100, 0, name)).getData().get(0);
}
}

228
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfilePr
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
@ -89,10 +90,15 @@ import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.noSec;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT;
@ -179,21 +185,97 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
" }";
public static String TELEMETRY_WITH_MANY_OBSERVE =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"/3_1.2/0/20\": \"batteryStatus\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\"\n" +
" ],\n" +
" \"attributeLwm2m\": {},\n" +
" \"observeStrategy\": 0\n" +
" }";
public static String TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3 =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"/5_1.2/0/3\": \"state\",\n" +
" \"/5_1.2/0/5\": \"updateResult\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {},\n" +
" \"observeStrategy\": 0\n" +
" }";
public static String TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19 =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/9\": \"batteryLevel\",\n" +
" \"/3_1.2/0/20\": \"batteryStatus\"\n" +
" \"/3_1.2/0/20\": \"batteryStatus\",\n" +
" \"/19_1.1/0/2\": \"dataCreationTime\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\"\n" +
" \"/3_1.2/0/20\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/9\",\n" +
" \"/3_1.2/0/20\"\n" +
" \"/3_1.2/0/20\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attributeLwm2m\": {},\n" +
" \"observeStrategy\": 1\n" +
" }";
public static String TELEMETRY_WITH_COMPOSITE_BY_OBJECT_OBSERVE_ID_3_ID_5_ID_19 =
" {\n" +
" \"keyName\": {\n" +
" \"/3_1.2/0/20\": \"batteryStatus\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/19_1.1/0/2\": \"dataCreationTime\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/3_1.2/0/20\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/3_1.2/0/20\",\n" +
" \"/5_1.2/0/6\",\n" +
" \"/19_1.1/0/2\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" \"attributeLwm2m\": {},\n" +
" \"observeStrategy\": 2\n" +
" }";
public static final String CLIENT_LWM2M_SETTINGS =
@ -208,6 +290,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
" \"pagingTransmissionWindow\": null,\n" +
" \"clientOnlyObserveAfterConnect\": 1\n" +
" }";
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
@ -247,11 +330,12 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
}
}
public void basicTestConnectionObserveTelemetry(Security security,
LwM2MDeviceCredentials deviceCredentials,
String endpoint,
boolean queueMode) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITHOUT_OBSERVE, getBootstrapServerCredentialsNoSec(NONE));
public void basicTestConnectionObserveSingleTelemetry(Security security,
LwM2MDeviceCredentials deviceCredentials,
String endpoint,
boolean queueMode,
boolean isUpdateProfile) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
@ -268,7 +352,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
getWsClient().registerWaitForUpdate();
this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString());
awaitObserveReadAll(0, lwM2MTestClient.getDeviceIdStr());
awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr());
String msg = getWsClient().waitForUpdate();
EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
@ -279,13 +363,127 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
Assert.assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class));
assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class));
int expectedMax = 50;
int expectedMin = 5;
Assert.assertTrue(expectedMax >= Long.parseLong(tsValue.getValue()));
Assert.assertTrue(expectedMin <= Long.parseLong(tsValue.getValue()));
if (isUpdateProfile) {
String actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
String expectedReadAll = "[\"SingleObservation:/3/0/9\"]";
assertEquals(expectedReadAll, actualResultReadAll);
updateProfile(deviceProfile, TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, SINGLE);
awaitObserveReadAll(6, lwM2MTestClient.getDeviceIdStr());
String expectedReadAll_3_9 = "\"SingleObservation:/3/0/9\"";
String expectedReadAll_5_5 = "\"SingleObservation:/5/0/5\"";
String expectedReadAll_5_6 = "\"SingleObservation:/5/0/6\"";
String expectedReadAll_5_7 = "\"SingleObservation:/5/0/7\"";
String expectedReadAll_5_9 = "\"SingleObservation:/5/0/9\"";
actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_9));
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_5));
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_6));
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_7));
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_9));
updateProfile(deviceProfile, TELEMETRY_WITH_MANY_OBSERVE, SINGLE);
awaitObserveReadAll(2, lwM2MTestClient.getDeviceIdStr());
String expectedReadAll_3_20 = "\"SingleObservation:/3/0/20\"";
actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_9));
assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_20));
}
}
public void basicTestConnectionObserveCompositeTelemetry(Security security,
LwM2MDeviceCredentials deviceCredentials,
String endpoint,
Lwm2mDeviceProfileTransportConfiguration transportConfiguration,
int cntObserve,
int varTest) throws Exception {
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
LatestValueCmd latestCmd = new LatestValueCmd();
String key1 = "pkgname";
String key2 = "pkgversion";
String key3 = "batteryLevel";
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key1)));
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key2)));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(3, edq, null, latestCmd, null);
getWsClient().send(cmd);
getWsClient().waitForReply();
getWsClient().registerWaitForUpdate();
this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString());
awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr());
String msg = getWsClient().waitForUpdate();
EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(3, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue1 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key1);
var tsValue2 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key2);
var tsValue3 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key3);
var value = tsValue1 != null ? tsValue1.getValue() : tsValue2.getValue();
if (tsValue3 != null) {
assertThat(Long.parseLong(tsValue3.getValue()), instanceOf(Long.class));
int expectedMax = 50;
int expectedMin = 5;
Assert.assertTrue(expectedMax >= Long.parseLong(tsValue3.getValue()));
Assert.assertTrue(expectedMin <= Long.parseLong(tsValue3.getValue()));
} else {
assertNotNull(value);
}
String expectedReadAll;
String actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
if (varTest == 0) {
expectedReadAll = "[\"CompositeObservation: [/5/0/9, /3/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\"]";
assertEquals(expectedReadAll, actualResultReadAll);
updateProfile(deviceProfile, TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19, COMPOSITE_ALL);
awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr());
expectedReadAll = "[\"CompositeObservation: [/19/0/2, /3/0/20, /3/0/9]\"]";
actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals(expectedReadAll, actualResultReadAll);
} else if (varTest == 1) {
String expectedReadAll3 = "\"CompositeObservation: [/3/0/9]\"";
String expectedReadAll5 = "\"CompositeObservation: [/5/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\"";
assertTrue(actualResultReadAll.contains(expectedReadAll3));
assertTrue(actualResultReadAll.contains(expectedReadAll5));
updateProfile(deviceProfile, TELEMETRY_WITH_COMPOSITE_BY_OBJECT_OBSERVE_ID_3_ID_5_ID_19, COMPOSITE_BY_OBJECT);
awaitObserveReadAll(3, lwM2MTestClient.getDeviceIdStr());
String expectedReadAll_3 = "\"CompositeObservation: [/3/0/20]\"";
String expectedReadAll_5 = "\"CompositeObservation: [/3/0/20]\"";
String expectedReadAll_19 = "\"CompositeObservation: [/19/0/2]\"";
actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(actualResultReadAll.contains(expectedReadAll_3));
assertTrue(actualResultReadAll.contains(expectedReadAll_5));
assertTrue(actualResultReadAll.contains(expectedReadAll_19));
} else if (varTest == 2) {
String expectedReadAll3 = "\"CompositeObservation: [/3/0/9]\"";
String expectedReadAll5 = "\"CompositeObservation: [/5/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\"";
assertTrue(actualResultReadAll.contains(expectedReadAll3));
assertTrue(actualResultReadAll.contains(expectedReadAll5));
updateProfile(deviceProfile, TELEMETRY_WITH_MANY_OBSERVE, SINGLE);
awaitObserveReadAll(2, lwM2MTestClient.getDeviceIdStr());
String expectedReadAll_3_9 = "\"SingleObservation:/3/0/9\"";
String expectedReadAll_3_20 = "\"SingleObservation:/3/0/20\"";
actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertTrue(actualResultReadAll.contains(expectedReadAll_3_9));
assertTrue(actualResultReadAll.contains(expectedReadAll_3_20));
}
}
protected DeviceProfile createLwm2mDeviceProfile(String name, Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception {
@ -308,6 +506,14 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return lwm2mDeviceProfile;
}
protected void updateProfile(DeviceProfile deviceProfile, String telemetryObserve, TelemetryObserveStrategy telemetryObserveStrategy) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(telemetryObserve, getBootstrapServerCredentialsNoSec(NONE));
transportConfiguration.getObserveAttr().setObserveStrategy(telemetryObserveStrategy);
DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + deviceProfile.getId().getId().toString(), DeviceProfile.class);
foundDeviceProfile.getProfileData().setTransportConfiguration(transportConfiguration);
doPost("/api/deviceProfile", foundDeviceProfile, DeviceProfile.class);
}
protected Device createLwm2mDevice(LwM2MDeviceCredentials credentials, String endpoint, DeviceProfileId deviceProfileId) throws Exception {
Device device = new Device();
device.setName(endpoint);

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java

@ -27,9 +27,6 @@ import org.eclipse.leshan.core.response.WriteResponse;
import javax.security.auth.Destroyable;
import java.sql.Time;
import java.time.Instant;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@ -187,8 +184,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
}
private Time getTimestamp() {
LocalTime localTime = LocalTime.ofInstant(Instant.now(), ZoneId.systemDefault());
this.timestamp = Time.valueOf(localTime);
setTimestamp();
return this.timestamp;
}

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java

@ -257,7 +257,9 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
}
private int getBatteryStatus() {
return RANDOM.nextInt(7);
int status = RANDOM.nextInt(7);
log.error("getBatteryStatus: [{}]", status);
return status;
}
private long getMemoryTotal() {

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java

@ -40,13 +40,15 @@ import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.*;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_CHECKSUM256;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_NAME;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_SIZE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_TITLE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_VERSION;
@Slf4j
@DaoSqlTest

3
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult;
import java.util.List;
import java.util.Set;
@ -258,7 +259,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
.filter(invocation ->
invocation.getMethod().getName().equals("updateAttrTelemetry") &&
invocation.getArguments().length > 1 &&
idVerRez.equals(invocation.getArguments()[1])
((ResourceUpdateResult)invocation.getArguments()[0]).getPaths().toString().contains(idVerRez)
)
.count();
}

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java

@ -67,7 +67,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "instance " + OBJECT_INSTANCE_ID_0 + " already exists";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
assertEquals(actual, expected);
}
/**
@ -84,7 +84,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Path " + expectedPath + ". Object must be Multiple !";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
assertEquals(actual, expected);
}
/**
@ -122,7 +122,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId);
String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
assertEquals(actual, expected);
}
private String sendRPCreateById(String path, String value) throws Exception {

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java

@ -20,11 +20,11 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals;
@ -80,7 +80,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
.updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null));
}
/**
@ -95,7 +95,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT
int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
.updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null));
}
/**
@ -328,7 +328,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT
int cntUpdate = 10;
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
.updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null));
}
private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception {

14
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

@ -19,12 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -140,24 +138,24 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
}
/**
* ReadComposite {"ids":["/1_1.2", "/3_1.0/0/1", "/3_1.0/0/11"]}
* ReadComposite {"ids":["/19_1.1", "/3_1.0/0/1", "/3_1.0/0/11"]}
*/
@Test
public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception {
String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get();
String objectId_1 = pathIdVerToObjectId(expectedIdVer_1);
String expectedIdVer_19 = "/" + BINARY_APP_DATA_CONTAINER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version;
String objectId_19 = pathIdVerToObjectId(expectedIdVer_19);
String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1;
String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11;
String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3);
String expectedIds = "[\"" + expectedIdVer_1 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]";
String expectedIds = "[\"" + expectedIdVer_19 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]";
String actualResult = sendCompositeRPCByIds(expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected1 = objectId_1 + "=LwM2mObject [id=" + new LwM2mPath(objectId_1).getObjectId() + ", instances={";
String expected19 = objectId_19 + "=LwM2mObject [id=" + new LwM2mPath(objectId_19).getObjectId() + ", instances={";
String expected3_0_1 = objectInstanceId_3 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value=";
String expected3_0_11 = objectInstanceId_3 + "/" + RESOURCE_ID_11 + "=LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={";
String actualValues = rpcActualResult.get("value").asText();
assertTrue(actualValues.contains(expected1));
assertTrue(actualValues.contains(expected19));
assertTrue(actualValues.contains(expected3_0_1));
assertTrue(actualValues.contains(expected3_0_11));
}

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -119,7 +119,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK
protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name")
protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK
private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"};
private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml", "19.xml"};
private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials;
@ -473,8 +473,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
protected String sendRPCSecurityExecuteById(String path, String deviceId, String endpoint) throws Exception {
log.info("endpoint1: [{}]", endpoint);
String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
}

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java

@ -32,13 +32,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT
public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC;
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.basicTestConnectionObserveTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false);
}
@Test
public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveTelemetry() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode";
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.basicTestConnectionObserveTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true);
super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false);
}
// Bootstrap + Lwm2m

42
application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java

@ -0,0 +1,42 @@
/**
* 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.transport.lwm2m.transportConfiguration;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class ObserveStrategyTransportConfigurationTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testTransportConfigurationObserveStrategyBeforeParseNullAfterParseNotNull_STRATEGY_SINGLE() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITHOUT_OBSERVE, getBootstrapServerCredentialsNoSec(NONE));
Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy());
Assert.assertEquals(SINGLE, transportConfiguration.getObserveAttr().getObserveStrategy());
}
@Test
public void testTransportConfigurationObserveStrategyBeforeParseNotNullAfterParseNotNull_STRATEGY_COMPOSITE_ALL() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19, getBootstrapServerCredentialsNoSec(NONE));
Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy());
Assert.assertEquals(COMPOSITE_ALL, transportConfiguration.getObserveAttr().getObserveStrategy());
}
}

63
application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java

@ -0,0 +1,63 @@
/**
* 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.transport.lwm2m.transportConfiguration;
import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL;
import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class ObserveStrategyWithNoSecQueueModeConnectTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetryUpdateProfileAfterConnected() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveSingle";
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true, true);
}
@Test
public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry_Both_UpdateProfileAfterConnected() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeAll";
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE));
transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_ALL);
super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1, 0);
}
@Test
public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry_Both_UpdateProfileAfterConnected() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeByObject";
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE));
transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT);
super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2, 1);
}
@Test
public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry_Single_UpdateProfileAfterConnected() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeByObject_Single";
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE));
transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT);
super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2, 2);
}
}

4
common/cluster-api/pom.xml

@ -60,10 +60,6 @@
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

4
common/dao-api/pom.xml

@ -56,10 +56,6 @@
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

29
common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.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.dao.trendz;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
public interface TrendzSettingsService {
void saveTrendzSettings(TenantId tenantId, TrendzSettings settings);
TrendzSettings findTrendzSettings(TenantId tenantId);
void deleteTrendzSettings(TenantId tenantId);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -35,6 +35,7 @@ public class CacheConstants {
public static final String DEVICE_PROFILE_CACHE = "deviceProfiles";
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings";
public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes";

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import java.util.List;
@ -37,4 +38,5 @@ public class SystemParams {
String calculatedFieldDebugPerTenantLimitsConfiguration;
long maxArgumentsPerCF;
long maxDataPointsPerRollingArg;
TrendzSettings trendzSettings;
}

8
common/data/src/main/java/org/thingsboard/server/common/data/TbProperty.java

@ -15,14 +15,16 @@
*/
package org.thingsboard.server.common.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Created by ashvayka on 25.09.18.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TbProperty {
private String key;
private String value;
}

22
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java

@ -15,17 +15,18 @@
*/
package org.thingsboard.server.common.data.device.profile.lwm2m;
import lombok.AllArgsConstructor;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TelemetryMappingConfiguration implements Serializable {
private static final long serialVersionUID = -7594999741305410419L;
@ -35,5 +36,22 @@ public class TelemetryMappingConfiguration implements Serializable {
private Set<String> attribute;
private Set<String> telemetry;
private Map<String, ObjectAttributes> attributeLwm2m;
private TelemetryObserveStrategy observeStrategy;
@JsonCreator
public TelemetryMappingConfiguration(
@JsonProperty("keyName") Map<String, String> keyName,
@JsonProperty("observe") Set<String> observe,
@JsonProperty("attribute") Set<String> attribute,
@JsonProperty("telemetry") Set<String> telemetry,
@JsonProperty("attributeLwm2m") Map<String, ObjectAttributes> attributeLwm2m,
@JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) {
this.keyName = keyName != null ? keyName : Collections.emptyMap();
this.observe = observe != null ? observe : Collections.emptySet();
this.attribute = attribute != null ? attribute : Collections.emptySet();
this.telemetry = telemetry != null ? telemetry : Collections.emptySet();
this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap();
this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE;
}
}

59
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java

@ -0,0 +1,59 @@
/**
* 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.device.profile.lwm2m;
import lombok.Getter;
public enum TelemetryObserveStrategy {
SINGLE("One resource equals one single observe request", 0),
COMPOSITE_ALL("All resources in one composite observe request", 1),
COMPOSITE_BY_OBJECT("Grouped composite observe requests by object", 2);
@Getter
private final String description;
@Getter
private final int id;
TelemetryObserveStrategy(String description, int id) {
this.description = description;
this.id = id;
}
public static TelemetryObserveStrategy fromDescription(String description) {
for (TelemetryObserveStrategy strategy : values()) {
if (strategy.description.equalsIgnoreCase(description)) {
return strategy;
}
}
throw new IllegalArgumentException("Unknown TelemetryObserveStrategy id: " + description);
}
public static TelemetryObserveStrategy fromId(int id) {
for (TelemetryObserveStrategy strategy : values()) {
if (strategy.id == id) {
return strategy;
}
}
throw new IllegalArgumentException("Unknown TelemetryObserveStrategy id: " + id);
}
@Override
public String toString() {
return name() + " (" + id + "): " + description;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java

@ -31,7 +31,8 @@ public enum ThingsboardErrorCode {
TOO_MANY_UPDATES(34),
VERSION_CONFLICT(35),
SUBSCRIPTION_VIOLATION(40),
PASSWORD_VIOLATION(45);
PASSWORD_VIOLATION(45),
DATABASE(46);
private int errorCode;

1
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java

@ -43,6 +43,7 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject {
public static final String EDGE_TYPE = "ManagedByEdge";
public static final String CONTAINS_TYPE = "Contains";
public static final String MANAGES_TYPE = "Manages";
public static final String USES_TYPE = "Uses";
@Setter
private EntityId from;

6
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java

@ -45,11 +45,15 @@ public class EntityLoadError implements Serializable {
}
public static EntityLoadError runtimeError(Throwable e) {
return runtimeError(e, null);
}
public static EntityLoadError runtimeError(Throwable e, EntityId externalId) {
String message = e.getMessage();
if (StringUtils.isEmpty(message)) {
message = "unexpected error (" + ClassUtils.getShortClassName(e.getClass()) + ")";
}
return EntityLoadError.builder().type("RUNTIME").message(message).build();
return EntityLoadError.builder().type("RUNTIME").message(message).source(externalId).build();
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java

@ -0,0 +1,26 @@
/**
* 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.trendz;
import lombok.Data;
@Data
public class TrendzSettings {
private boolean enabled;
private String baseUrl;
}

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

Loading…
Cancel
Save