Browse Source

Merge remote-tracking branch 'ce/master' into fix-rule-chain-metadata-for-old-edge-ce

pull/12843/head
yevhenii 2 years ago
parent
commit
197cd6da21
  1. 5
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  2. 234
      application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg
  3. 466
      application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg
  4. 582
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  5. 200
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  6. 232
      application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg
  7. 466
      application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg
  8. 234
      application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg
  9. 201
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  10. 200
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  11. 466
      application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg
  12. 234
      application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg
  13. 466
      application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg
  14. 200
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  15. 8
      application/src/main/data/json/system/widget_bundles/maps.json
  16. 4
      application/src/main/data/json/system/widget_types/google_map.json
  17. 4
      application/src/main/data/json/system/widget_types/here_map.json
  18. 95
      application/src/main/data/json/system/widget_types/image_map.json
  19. 102
      application/src/main/data/json/system/widget_types/image_map_deprecated.json
  20. 44
      application/src/main/data/json/system/widget_types/map.json
  21. 4
      application/src/main/data/json/system/widget_types/markers_placement___google_maps.json
  22. 4
      application/src/main/data/json/system/widget_types/markers_placement___image_map.json
  23. 4
      application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json
  24. 4
      application/src/main/data/json/system/widget_types/openstreet_map.json
  25. 80
      application/src/main/data/json/system/widget_types/route_map.json
  26. 2
      application/src/main/data/json/system/widget_types/route_map___google.json
  27. 2
      application/src/main/data/json/system/widget_types/route_map___openstreet.json
  28. 2
      application/src/main/data/json/system/widget_types/route_map___tencent.json
  29. 4
      application/src/main/data/json/system/widget_types/tencent_map.json
  30. 2
      application/src/main/data/json/system/widget_types/trip_animation.json
  31. 47
      application/src/main/data/json/system/widget_types/trip_map.json
  32. 5
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  33. 5
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  34. 86
      application/src/main/data/upgrade/basic/schema_update.sql
  35. 16
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  36. 24
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  37. 14
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  38. 21
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  39. 32
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  40. 28
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  41. 10
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  42. 7
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  43. 8
      application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java
  44. 14
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  45. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  46. 25
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  47. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  48. 18
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  49. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  50. 17
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  51. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  52. 8
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  53. 83
      application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java
  54. 76
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  55. 11
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  56. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
  57. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  58. 3
      application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java
  59. 143
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  60. 14
      application/src/main/resources/thingsboard.yml
  61. 79
      application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java
  62. 6
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  63. 5
      application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java
  64. 8
      application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java
  65. 6
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  66. 759
      application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
  67. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  68. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java
  69. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java
  70. 8
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  71. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java
  72. 3
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java
  73. 13
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java
  74. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java
  75. 6
      common/proto/src/main/proto/queue.proto
  76. 41
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  77. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
  78. 3
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java
  79. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java
  80. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  81. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  82. 6
      common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java
  83. 54
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  84. 14
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java
  85. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java
  86. 7
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  87. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  88. 21
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java
  89. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java
  90. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java
  91. 8
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  92. 4
      common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java
  93. 1
      common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java
  94. 3
      dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java
  95. 1
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  96. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  97. 2
      dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql
  98. 4
      docker/docker-compose.edqs.volumes.yml
  99. 12
      docker/docker-compose.edqs.yml
  100. 98
      docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json

5
application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json

@ -50,8 +50,11 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"configurationVersion": 2, "configurationVersion": 3,
"configuration": { "configuration": {
"processingSettings": {
"type": "ON_EVERY_MESSAGE"
},
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": false, "notifyDevice": false,
"sendAttributesUpdatedNotification": false, "sendAttributesUpdatedNotification": false,

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

@ -3,6 +3,7 @@
"description": "Bottom right elbow connector", "description": "Bottom right elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -10,23 +11,132 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M200 101H131C113.879 101 100 114.879 100 132V201" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/> <path d="M200 101H131C113.879 101 100 114.879 100 132V201" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

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

@ -3,6 +3,7 @@
"description": "Bottom tee connector", "description": "Bottom tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -15,23 +16,364 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"id": "leftFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.fluid-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100L200 100" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/> <path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100L200 100" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="rightLine"/><g tb:tag="bottomLine"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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

@ -3,6 +3,7 @@
"description": "Cross connector", "description": "Cross connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -15,23 +16,480 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"id": "leftFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.fluid-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -42,12 +500,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -56,32 +513,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" id="path12" fill="#1A1A1A" tb:tag="line-color"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" id="path10" fill="#1A1A1A" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke-width="6" id="path8" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke-width="6" id="path6" stroke="#1A1A1A" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke-width="6" id="path4" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke-width="6" id="path2" stroke="#1A1A1A" tb:tag="line"/> <path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" id="path12" fill="#1A1A1A" tb:tag="line-color"/><path d="M87 100H113C113 100 100 100 100 114C100 100 87 100 87 100Z" id="path10" fill="#1A1A1A" tb:tag="line-color"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke-width="6" id="path8" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke-width="6" id="path6" stroke="#1A1A1A" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 106.716 100 115V200" stroke-width="6" id="path4" stroke="#1A1A1A" tb:tag="line"/><path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke-width="6" id="path2" stroke="#1A1A1A" tb:tag="line"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="rightLine"/><g tb:tag="bottomLine"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

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

@ -3,6 +3,7 @@
"description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "description": "Horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -85,6 +86,83 @@
}, },
"defaultSetValueSettings": null, "defaultSetValueSettings": null,
"defaultWidgetActionSettings": null "defaultWidgetActionSettings": null
},
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
} }
], ],
"properties": [ "properties": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#C8DFF7"
"required": null, },
"subLabel": null, {
"divider": null, "id": "flowStyleDash",
"fieldSuffix": null, "name": "{i18n:scada.symbol.flow-style}",
"disableOnProperty": null, "group": "{i18n:scada.symbol.animation}",
"rowClass": "", "type": "number",
"fieldClass": "", "default": 10,
"min": null, "required": true,
"max": null, "subLabel": "{i18n:scada.symbol.dash}",
"step": null "divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M0 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M129 100L71 129V71L129 100Z" fill="#1A1A1A" tb:tag="arrow"/> <path d="M0 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="M129 100L71 129V71L129 100Z" fill="#1A1A1A" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

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

@ -3,6 +3,7 @@
"description": "Left bottom elbow connector", "description": "Left bottom elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -10,23 +11,132 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -51,32 +160,93 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7"
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 200L100 131C100 113.879 86.1208 100 69 100L0 100" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/> <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"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

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

@ -3,6 +3,7 @@
"description": "Left tee connector", "description": "Left tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -15,23 +16,364 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"id": "leftFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.fluid-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></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"/> <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>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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

@ -3,6 +3,7 @@
"description": "Left top elbow connector", "description": "Left top elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -10,23 +11,132 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M0 100H69C86.1208 100 100 86.1208 100 69V0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/> <path d="M0 100H69C86.1208 100 100 86.1208 100 69V0" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

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

@ -4,6 +4,7 @@
"description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 2, "widgetSizeX": 2,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -86,6 +87,83 @@
}, },
"defaultSetValueSettings": null, "defaultSetValueSettings": null,
"defaultWidgetActionSettings": null "defaultWidgetActionSettings": null
},
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
} }
], ],
"properties": [ "properties": [
@ -94,16 +172,8 @@
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -114,12 +184,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -128,49 +197,93 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#C8DFF7"
"required": null, },
"subLabel": null, {
"divider": null, "id": "flowStyleDash",
"fieldSuffix": null, "name": "{i18n:scada.symbol.flow-style}",
"disableOnProperty": null, "group": "{i18n:scada.symbol.animation}",
"rowClass": "", "type": "number",
"fieldClass": "", "default": 10,
"min": null, "required": true,
"max": null, "subLabel": "{i18n:scada.symbol.dash}",
"step": null "divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/> <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="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

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

@ -3,6 +3,7 @@
"description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "description": "Long vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 2, "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",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -85,6 +86,83 @@
}, },
"defaultSetValueSettings": null, "defaultSetValueSettings": null,
"defaultWidgetActionSettings": null "defaultWidgetActionSettings": null
},
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
} }
], ],
"properties": [ "properties": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#C8DFF7"
"required": null, },
"subLabel": null, {
"divider": null, "id": "flowStyleDash",
"fieldSuffix": null, "name": "{i18n:scada.symbol.flow-style}",
"disableOnProperty": null, "group": "{i18n:scada.symbol.animation}",
"rowClass": "", "type": "number",
"fieldClass": "", "default": 10,
"min": null, "required": true,
"max": null, "subLabel": "{i18n:scada.symbol.dash}",
"step": null "divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></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"/> <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"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

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

@ -3,6 +3,7 @@
"description": "Right tee connector", "description": "Right tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -15,23 +16,364 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"id": "topFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.fluid-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "bottomFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.bottom-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 114 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/> <path d="M200 100H115C106.716 100 100 106.716 100 115V200" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M100 0V200" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M100 113V87C100 87 100 100 114 100C100 100 100 113 100 113Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><g tb:tag="animationGroup"><g tb:tag="topLine"/><g tb:tag="rightLine"/><g tb:tag="bottomLine"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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

@ -3,6 +3,7 @@
"description": "Top right elbow connector", "description": "Top right elbow connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -10,23 +11,132 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "animationDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M100 0V69C100 86.1208 113.879 100 131 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/> <path d="M100 0V69C100 86.1208 113.879 100 131 100H200" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><g tb:tag="animationGroup"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

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

@ -3,6 +3,7 @@
"description": "Top tee connector", "description": "Top tee connector",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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}",
"tags": [ "tags": [
{ {
"tag": "line", "tag": "line",
@ -15,23 +16,364 @@
"actions": null "actions": null
} }
], ],
"behavior": [], "behavior": [
{
"id": "leftFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "leftFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.left-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.fluid-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowDirection",
"name": "{i18n:scada.symbol.animation-direction}",
"hint": "{i18n:scada.symbol.animation-direction-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "topFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.top-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlow",
"name": "{i18n:scada.symbol.flow-animation}",
"hint": "{i18n:scada.symbol.flow-animation-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowDirection",
"name": "{i18n:scada.symbol.flow-direction}",
"hint": "{i18n:scada.symbol.flow-direction-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": "{i18n:scada.symbol.forward}",
"falseLabel": "{i18n:scada.symbol.reverse}",
"stateLabel": "{i18n:scada.symbol.forward}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": true,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "rightFlowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": "{i18n:scada.symbol.right-connector}",
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [ "properties": [
{ {
"id": "mainLine", "id": "mainLine",
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null, },
"fieldSuffix": null, {
"disableOnProperty": null, "id": "flowAnimationWidth",
"rowClass": "", "name": "{i18n:scada.symbol.flow}",
"fieldClass": "", "group": "{i18n:scada.symbol.animation}",
"min": null, "type": "number",
"max": null, "default": 4,
"step": null "subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color",
"default": "#C8DFF7",
"disabled": false,
"visible": true
},
{
"id": "flowStyleDash",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"required": true,
"subLabel": "{i18n:scada.symbol.dash}",
"divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>
<path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100L200 100" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/> <path d="M200 100H115C106.716 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path2" tb:tag="line"/><path d="M0 100H85C93.2843 100 100 93.2843 100 85V0" stroke="#1A1A1A" stroke-width="6" id="path4" tb:tag="line"/><path d="M0 100L200 100" stroke="#1A1A1A" stroke-width="6" id="path6" tb:tag="line"/><path d="M87 100H113C113 100 100 100 100 86C100 100 87 100 87 100Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="2" id="path8" tb:tag="line-color"/><g tb:tag="animationGroup"><g tb:tag="leftLine"/><g tb:tag="topLine"/><g tb:tag="rightLine"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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

@ -3,6 +3,7 @@
"description": "Vertical connector with an optional directional arrow to visually indicate flow.", "description": "Vertical connector with an optional directional arrow to visually indicate flow.",
"widgetSizeX": 1, "widgetSizeX": 1,
"widgetSizeY": 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",
"tags": [ "tags": [
{ {
"tag": "arrow", "tag": "arrow",
@ -85,6 +86,83 @@
}, },
"defaultSetValueSettings": null, "defaultSetValueSettings": null,
"defaultWidgetActionSettings": null "defaultWidgetActionSettings": null
},
{
"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.present}",
"falseLabel": "{i18n:scada.symbol.absent}",
"stateLabel": "{i18n:scada.symbol.flow-present}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "flowAnimationSpeed",
"name": "{i18n:scada.symbol.flow-animation-speed}",
"hint": "{i18n:scada.symbol.flow-animation-speed-hint}",
"group": null,
"type": "value",
"valueType": "DOUBLE",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": 1,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "state"
},
"getAlarmStatus": {
"severityList": null,
"typeList": null
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
} }
], ],
"properties": [ "properties": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}", "name": "{i18n:scada.symbol.main-line}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "mainLineSize", "id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main", "subLabel": "Main",
"divider": true, "divider": true,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "secondaryLineSize", "id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2, "default": 2,
"required": true, "required": true,
"subLabel": "Secondary", "subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px", "fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 99, "max": 99,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "lineColor", "id": "lineColor",
"name": "{i18n:scada.symbol.line-color}", "name": "{i18n:scada.symbol.line-color}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#1A1A1A",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "flowAnimationWidth",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 4,
"subLabel": "Width",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowAnimationColor",
"name": "{i18n:scada.symbol.flow}",
"group": "{i18n:scada.symbol.animation}",
"type": "color", "type": "color",
"default": "#1A1A1A", "default": "#C8DFF7"
"required": null, },
"subLabel": null, {
"divider": null, "id": "flowStyleDash",
"fieldSuffix": null, "name": "{i18n:scada.symbol.flow-style}",
"disableOnProperty": null, "group": "{i18n:scada.symbol.animation}",
"rowClass": "", "type": "number",
"fieldClass": "", "default": 10,
"min": null, "required": true,
"max": null, "subLabel": "{i18n:scada.symbol.dash}",
"step": null "divider": true,
"fieldSuffix": "px",
"min": 0,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowStyleGap",
"name": "{i18n:scada.symbol.flow-style}",
"group": "{i18n:scada.symbol.animation}",
"type": "number",
"default": 10,
"subLabel": "{i18n:scada.symbol.gap}",
"fieldSuffix": "px",
"min": 1,
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "flowDashCap",
"name": "{i18n:scada.symbol.flow-dash-cap}",
"group": "{i18n:scada.symbol.animation}",
"type": "select",
"default": "butt",
"items": [
{
"value": "butt",
"label": "{i18n:scada.symbol.dash-cap-butt}"
},
{
"value": "round",
"label": "{i18n:scada.symbol.dash-cap-round}"
},
{
"value": "square",
"label": "{i18n:scada.symbol.dash-cap-square}"
}
],
"disabled": false,
"visible": true
} }
] ]
}]]></tb:metadata> }]]></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"/> <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"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

8
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

95
application/src/main/data/json/system/widget_types/image_map.json

File diff suppressed because one or more lines are too long

102
application/src/main/data/json/system/widget_types/image_map_deprecated.json

File diff suppressed because one or more lines are too long

44
application/src/main/data/json/system/widget_types/map.json

File diff suppressed because one or more lines are too long

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

@ -1,7 +1,7 @@
{ {
"fqn": "input_widgets.markers_placement_google_maps", "fqn": "input_widgets.markers_placement_google_maps",
"name": "Markers Placement - Google Maps", "name": "Markers Placement - Google Maps",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/markers_placement_google_maps_system_widget_image.png", "image": "tb-image;/api/images/system/markers_placement_google_maps_system_widget_image.png",
"description": "Allows configuring the location of the selected entities on Google Maps. By default, store the location using 'latitude' and 'longitude' server-side attributes.", "description": "Allows configuring the location of the selected entities on Google Maps. By default, store the location using 'latitude' and 'longitude' server-side attributes.",
"descriptor": { "descriptor": {
@ -14,7 +14,7 @@
"controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-map-widget-settings", "settingsDirective": "tb-map-widget-settings-legacy",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete_polygon'>Delete</link-act>\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete_polygon'>Delete</link-act>\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
}, },
"tags": [ "tags": [

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

@ -1,7 +1,7 @@
{ {
"fqn": "input_widgets.markers_placement_image_map", "fqn": "input_widgets.markers_placement_image_map",
"name": "Markers Placement - Image Map", "name": "Markers Placement - Image Map",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/markers_placement_image_map_system_widget_image.png", "image": "tb-image;/api/images/system/markers_placement_image_map_system_widget_image.png",
"description": "Allows configuring the location of the selected entities on the Image map. By default, store the location using 'xPos' and 'yPos' server-side attributes with values of 0.0 to 1.0.", "description": "Allows configuring the location of the selected entities on the Image map. By default, store the location using 'xPos' and 'yPos' server-side attributes with values of 0.0 to 1.0.",
"descriptor": { "descriptor": {
@ -14,7 +14,7 @@
"controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-map-widget-settings", "settingsDirective": "tb-map-widget-settings-legacy",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"tb-image;/api/images/system/markers_placement_image_map_system_widget_map_image.svg\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete_polygon'>Delete</link-act>\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"tb-image;/api/images/system/markers_placement_image_map_system_widget_map_image.svg\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete_polygon'>Delete</link-act>\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
}, },
"tags": [ "tags": [

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

@ -1,7 +1,7 @@
{ {
"fqn": "input_widgets.markers_placement_openstreetmap", "fqn": "input_widgets.markers_placement_openstreetmap",
"name": "Markers Placement - OpenStreetMap", "name": "Markers Placement - OpenStreetMap",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/markers_placement_openstreetmap_system_widget_image.png", "image": "tb-image;/api/images/system/markers_placement_openstreetmap_system_widget_image.png",
"description": "Allows configuring the location of the selected entities on OpenStreetMap. By default, store the location using 'latitude' and 'longitude' server-side attributes.", "description": "Allows configuring the location of the selected entities on OpenStreetMap. By default, store the location using 'latitude' and 'longitude' server-side attributes.",
"descriptor": { "descriptor": {
@ -14,7 +14,7 @@
"controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-map-widget-settings", "settingsDirective": "tb-map-widget-settings-legacy",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete'>Delete</link-act>\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><br/><link-act name='delete'>Delete</link-act>\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${coordinates|ts:7}<br/><br/><link-act name='delete'>Delete</link-act>\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
}, },
"tags": [ "tags": [

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

File diff suppressed because one or more lines are too long

80
application/src/main/data/json/system/widget_types/route_map.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/route_map___google.json

@ -1,7 +1,7 @@
{ {
"fqn": "maps_v2.route_map", "fqn": "maps_v2.route_map",
"name": "Route Map - Google", "name": "Route Map - Google",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/route_map_google_system_widget_image.png", "image": "tb-image;/api/images/system/route_map_google_system_widget_image.png",
"description": "Visualize the entity trip on Google Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "description": "Visualize the entity trip on Google Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.",
"descriptor": { "descriptor": {

2
application/src/main/data/json/system/widget_types/route_map___openstreet.json

@ -1,7 +1,7 @@
{ {
"fqn": "maps_v2.route_map_openstreetmap", "fqn": "maps_v2.route_map_openstreetmap",
"name": "Route Map - OpenStreet", "name": "Route Map - OpenStreet",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png", "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png",
"description": "Visualize the entity trip on OpenStreetMap. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "description": "Visualize the entity trip on OpenStreetMap. Allows to visualize location history. Use the Trip Animation widget for advanced features.",
"descriptor": { "descriptor": {

2
application/src/main/data/json/system/widget_types/route_map___tencent.json

@ -1,7 +1,7 @@
{ {
"fqn": "maps_v2.route_map_tencent_maps", "fqn": "maps_v2.route_map_tencent_maps",
"name": "Route Map - Tencent", "name": "Route Map - Tencent",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/route_map_tencent_system_widget_image.png", "image": "tb-image;/api/images/system/route_map_tencent_system_widget_image.png",
"description": "Visualize the entity trip on Tencent Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "description": "Visualize the entity trip on Tencent Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.",
"descriptor": { "descriptor": {

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

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/trip_animation.json

@ -1,7 +1,7 @@
{ {
"fqn": "maps_v2.test", "fqn": "maps_v2.test",
"name": "Trip Animation", "name": "Trip Animation",
"deprecated": false, "deprecated": true,
"image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png",
"description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.",
"descriptor": { "descriptor": {

47
application/src/main/data/json/system/widget_types/trip_map.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -35,8 +35,11 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"configurationVersion": 2, "configurationVersion": 3,
"configuration": { "configuration": {
"processingSettings": {
"type": "ON_EVERY_MESSAGE"
},
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": false, "notifyDevice": false,
"sendAttributesUpdatedNotification": false, "sendAttributesUpdatedNotification": false,

5
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -34,8 +34,11 @@
}, },
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"configurationVersion": 2, "configurationVersion": 3,
"configuration": { "configuration": {
"processingSettings": {
"type": "ON_EVERY_MESSAGE"
},
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": false, "notifyDevice": false,
"sendAttributesUpdatedNotification": false, "sendAttributesUpdatedNotification": false,

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

@ -16,52 +16,54 @@
-- UPDATE SAVE TIME SERIES NODES START -- UPDATE SAVE TIME SERIES NODES START
DO $$ UPDATE rule_node
BEGIN SET configuration = (
-- Check if the rule_node table exists (configuration::jsonb - 'skipLatestPersistence')
IF EXISTS ( || jsonb_build_object(
SELECT 1 'processingSettings', jsonb_build_object(
FROM information_schema.tables 'type', 'ADVANCED',
WHERE table_name = 'rule_node' 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'),
) THEN 'latest', jsonb_build_object('type', 'SKIP'),
'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'),
'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE')
)
)
)::text,
configuration_version = 1
WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
AND configuration_version = 0
AND configuration::jsonb ->> 'skipLatestPersistence' = 'true';
UPDATE rule_node UPDATE rule_node
SET configuration = ( SET configuration = (
(configuration::jsonb - 'skipLatestPersistence') (configuration::jsonb - 'skipLatestPersistence')
|| jsonb_build_object( || jsonb_build_object(
'processingSettings', jsonb_build_object( 'processingSettings', jsonb_build_object(
'type', 'ADVANCED', 'type', 'ON_EVERY_MESSAGE'
'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), )
'latest', jsonb_build_object('type', 'SKIP'), )
'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), )::text,
'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE') configuration_version = 1
) WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
) AND configuration_version = 0
)::text, AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL);
configuration_version = 1
WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
AND configuration_version = 0
AND configuration::jsonb ->> 'skipLatestPersistence' = 'true';
UPDATE rule_node -- UPDATE SAVE TIME SERIES NODES END
SET configuration = (
(configuration::jsonb - 'skipLatestPersistence')
|| jsonb_build_object(
'processingSettings', jsonb_build_object(
'type', 'ON_EVERY_MESSAGE'
)
)
)::text,
configuration_version = 1
WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
AND configuration_version = 0
AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL);
END IF; -- UPDATE SAVE ATTRIBUTES NODES START
END;
$$;
-- UPDATE SAVE TIME SERIES NODES END UPDATE rule_node
SET configuration = (
configuration::jsonb
|| jsonb_build_object(
'processingSettings', jsonb_build_object('type', 'ON_EVERY_MESSAGE')
)
)::text,
configuration_version = 3
WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode'
AND configuration_version = 2;
-- UPDATE SAVE ATTRIBUTES NODES END
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;

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

@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
@ -432,10 +431,6 @@ public class ActorSystemContext {
@Getter @Getter
private ClaimDevicesService claimDevicesService; private ClaimDevicesService claimDevicesService;
@Autowired
@Getter
private JsInvokeStats jsInvokeStats;
//TODO: separate context for TbCore and TbRuleEngine //TODO: separate context for TbCore and TbRuleEngine
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter
@ -595,17 +590,6 @@ public class ActorSystemContext {
this.localCacheType = "caffeine".equals(cacheType); this.localCacheType = "caffeine".equals(cacheType);
} }
@Scheduled(fixedDelayString = "${actors.statistics.js_print_interval_ms}")
public void printStats() {
if (statisticsEnabled) {
if (jsInvokeStats.getRequests() > 0 || jsInvokeStats.getResponses() > 0 || jsInvokeStats.getFailures() > 0) {
log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]",
jsInvokeStats.getRequests(), jsInvokeStats.getResponses(), jsInvokeStats.getFailures());
jsInvokeStats.reset();
}
}
}
@Value("${actors.tenant.create_components_on_init:true}") @Value("${actors.tenant.create_components_on_init:true}")
@Getter @Getter
private boolean tenantComponentsInitEnabled; private boolean tenantComponentsInitEnabled;

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

@ -24,6 +24,7 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
@ -74,7 +76,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
final EntityId entityId; final EntityId entityId;
final CalculatedFieldProcessingService cfService; final CalculatedFieldProcessingService cfService;
final CalculatedFieldStateService cfStateService; final CalculatedFieldStateService cfStateService;
final int partition;
TbActorCtx ctx; TbActorCtx ctx;
Map<CalculatedFieldId, CalculatedFieldState> states = new HashMap<>(); Map<CalculatedFieldId, CalculatedFieldState> states = new HashMap<>();
@ -85,7 +86,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
this.entityId = entityId; this.entityId = entityId;
this.cfService = systemContext.getCalculatedFieldProcessingService(); this.cfService = systemContext.getCalculatedFieldProcessingService();
this.cfStateService = systemContext.getCalculatedFieldStateService(); this.cfStateService = systemContext.getCalculatedFieldStateService();
this.partition = systemContext.getCalculatedFieldEntityProfileCache().getEntityIdPartition(tenantId, entityId);
} }
void init(TbActorCtx ctx) { void init(TbActorCtx ctx) {
@ -93,8 +93,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
public void process(CalculatedFieldPartitionChangeMsg msg) { public void process(CalculatedFieldPartitionChangeMsg msg) {
if (!msg.getPartitions()[partition]) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) {
log.info("[{}][{}] Stopping entity actor due to change partition event.", partition, entityId); log.info("[{}] Stopping entity actor due to change partition event.", entityId);
ctx.stop(ctx.getSelf()); ctx.stop(ctx.getSelf());
} }
} }
@ -134,20 +134,26 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(CalculatedFieldEntityDeleteMsg msg) { public void process(CalculatedFieldEntityDeleteMsg msg) {
log.info("[{}] Processing CF entity delete msg.", msg.getEntityId()); log.info("[{}] Processing CF entity delete msg.", msg.getEntityId());
if (this.entityId.equals(msg.getEntityId())) { if (this.entityId.equals(msg.getEntityId())) {
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); if (states.isEmpty()) {
states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); msg.getCallback().onSuccess();
ctx.stop(ctx.getSelf()); } else {
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback());
states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback));
ctx.stop(ctx.getSelf());
}
} else { } else {
var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var state = states.remove(cfId); var state = states.remove(cfId);
if (state != null) { if (state != null) {
cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback());
} else {
msg.getCallback().onSuccess();
} }
} }
} }
public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException {
log.info("[{}] Processing CF telemetry msg.", msg.getEntityId()); log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId());
var proto = msg.getProto(); var proto = msg.getProto();
var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size());
MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback());
@ -162,7 +168,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException {
log.info("[{}] Processing CF link telemetry msg.", msg.getEntityId()); log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId());
var proto = msg.getProto(); var proto = msg.getProto();
var ctx = msg.getCtx(); var ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback());

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

@ -199,7 +199,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (fieldsCount > 0) { if (fieldsCount > 0) {
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
var entityId = msg.getEntityId(); var entityId = msg.getEntityId();
oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), callback)); oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
} else { } else {
callback.onSuccess(); callback.onSuccess();
@ -306,7 +306,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (!entityIds.isEmpty()) { if (!entityIds.isEmpty()) {
//TODO: no need to do this if we cache all created actors and know which one belong to us; //TODO: no need to do this if we cache all created actors and know which one belong to us;
var multiCallback = new MultipleTbCallback(entityIds.size(), callback); var multiCallback = new MultipleTbCallback(entityIds.size(), callback);
entityIds.forEach(id -> deleteCfForEntity(entityId, cfId, multiCallback)); entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback));
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
@ -318,14 +318,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId(); EntityId entityId = msg.getEntityId();
log.info("Received telemetry msg from entity [{}]", entityId); log.debug("Received telemetry msg from entity [{}]", entityId);
// 2 = 1 for CF processing + 1 for links processing // 2 = 1 for CF processing + 1 for links processing
MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback());
// process all cfs related to entity, or it's profile; // process all cfs related to entity, or it's profile;
var entityIdFields = getCalculatedFieldsByEntityId(entityId); var entityIdFields = getCalculatedFieldsByEntityId(entityId);
var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId));
if (!entityIdFields.isEmpty() || !profileIdFields.isEmpty()) { if (!entityIdFields.isEmpty() || !profileIdFields.isEmpty()) {
log.info("Pushing telemetry msg to specific actor [{}]", entityId); log.debug("Pushing telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityCalculatedFieldTelemetryMsg(msg, entityIdFields, profileIdFields, callback)); getOrCreateActor(entityId).tell(new EntityCalculatedFieldTelemetryMsg(msg, entityIdFields, profileIdFields, callback));
} else { } else {
callback.onSuccess(); callback.onSuccess();
@ -342,7 +342,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) {
EntityId sourceEntityId = msg.getEntityId(); EntityId sourceEntityId = msg.getEntityId();
log.info("Received linked telemetry msg from entity [{}]", sourceEntityId); log.debug("Received linked telemetry msg from entity [{}]", sourceEntityId);
var proto = msg.getProto(); var proto = msg.getProto();
var linksList = proto.getLinksList(); var linksList = proto.getLinksList();
for (var linkProto : linksList) { for (var linkProto : linksList) {
@ -357,14 +357,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback());
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback);
entityIds.forEach(entityId -> { entityIds.forEach(entityId -> {
log.info("Pushing linked telemetry msg to specific actor [{}]", entityId); log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(newMsg); getOrCreateActor(entityId).tell(newMsg);
}); });
} else { } else {
msg.getCallback().onSuccess(); msg.getCallback().onSuccess();
} }
} else { } else {
log.info("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId);
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, msg.getCallback()); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, msg.getCallback());
getOrCreateActor(targetEntityId).tell(newMsg); getOrCreateActor(targetEntityId).tell(newMsg);
} }

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

@ -654,27 +654,6 @@ public class DefaultTbContext implements TbContext {
} }
} }
@Override
public void logJsEvalRequest() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeStats().incrementRequests();
}
}
@Override
public void logJsEvalResponse() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeStats().incrementResponses();
}
}
@Override
public void logJsEvalFailure() {
if (mainCtx.isStatisticsEnabled()) {
mainCtx.getJsInvokeStats().incrementFailures();
}
}
@Override @Override
public String getServiceId() { public String getServiceId() {
return mainCtx.getServiceInfoProvider().getServiceId(); return mainCtx.getServiceInfoProvider().getServiceId();

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

@ -91,6 +91,15 @@ public class TenantActor extends RuleChainManagerActor {
isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
if (isRuleEngine) { if (isRuleEngine) {
if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) {
try {
//TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0;
cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId),
() -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME,
() -> new CalculatedFieldManagerActorCreator(systemContext, tenantId),
() -> true);
} catch (Exception e) {
log.info("[{}] Failed to init CF Actor.", tenantId, e);
}
try { try {
if (getApiUsageState().isReExecEnabled()) { if (getApiUsageState().isReExecEnabled()) {
log.debug("[{}] Going to init rule chains", tenantId); log.debug("[{}] Going to init rule chains", tenantId);
@ -98,11 +107,6 @@ public class TenantActor extends RuleChainManagerActor {
} else { } else {
log.info("[{}] Skip init of the rule chains due to API limits", tenantId); log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
} }
//TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0;
cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId),
() -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME,
() -> new CalculatedFieldManagerActorCreator(systemContext, tenantId),
() -> true);
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
cantFindTenant = true; cantFindTenant = true;
@ -185,6 +189,10 @@ public class TenantActor extends RuleChainManagerActor {
} }
private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) {
if (cfActor == null) {
log.warn("[{}] CF Actor is not initialized.", tenantId);
return;
}
if (priority) { if (priority) {
cfActor.tellWithHighPriority(msg); cfActor.tellWithHighPriority(msg);
} else { } else {
@ -251,11 +259,25 @@ public class TenantActor extends RuleChainManagerActor {
ServiceType serviceType = msg.getServiceType(); ServiceType serviceType = msg.getServiceType();
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) {
if (cfActor == null) {
try {
//TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0;
cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId),
() -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME,
() -> new CalculatedFieldManagerActorCreator(systemContext, tenantId),
() -> true);
} catch (Exception e) {
log.info("[{}] Failed to init CF Actor.", tenantId, e);
}
}
if (!ruleChainsInitialized) { if (!ruleChainsInitialized) {
log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId); log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId);
initRuleChains(); initRuleChains();
} }
} else { } else {
if (cfActor != null) {
ctx.stop(cfActor.getActorId());
}
if (ruleChainsInitialized) { if (ruleChainsInitialized) {
log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId); log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId);
destroyRuleChains(); destroyRuleChains();

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

@ -34,6 +34,8 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
@ -216,11 +218,14 @@ public class CalculatedFieldController extends BaseController {
@RequestBody JsonNode inputParams) { @RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText(); String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse( Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<Map<String, TbelCfArg>>() { JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {
}), }),
Collections.emptyMap() Collections.emptyMap()
); );
ArrayList<String> argNames = new ArrayList<>(arguments.keySet());
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = ""; String output = "";
String errorText = ""; String errorText = "";
@ -234,12 +239,20 @@ public class CalculatedFieldController extends BaseController {
getTenantId(), getTenantId(),
tbelInvokeService, tbelInvokeService,
expression, expression,
argNames.toArray(String[]::new) ctxAndArgNames.toArray(String[]::new)
); );
Object[] args = argNames.stream()
.map(arguments::get) Object[] args = new Object[ctxAndArgNames.size()];
.toArray(); args[0] = new TbelCfCtx(arguments);
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json); output = JacksonUtil.toString(json);
@ -260,7 +273,8 @@ public class CalculatedFieldController extends BaseController {
EntityType entityType = referencedEntityId.getEntityType(); EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) { switch (entityType) {
case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
} }
} }

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

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -160,7 +161,12 @@ public class TenantProfileController extends BaseController {
" \"rpcTtlDays\": 0,\n" + " \"rpcTtlDays\": 0,\n" +
" \"queueStatsTtlDays\": 0,\n" + " \"queueStatsTtlDays\": 0,\n" +
" \"ruleEngineExceptionsTtlDays\": 0,\n" + " \"ruleEngineExceptionsTtlDays\": 0,\n" +
" \"warnThreshold\": 0\n" + " \"warnThreshold\": 0,\n" +
" \"maxCalculatedFieldsPerEntity\": 5,\n" +
" \"maxArgumentsPerCF\": 10,\n" +
" \"maxDataPointsPerRollingArg\": 1000,\n" +
" \"maxStateSizeInKBytes\": 32,\n" +
" \"maxSingleValueArgumentSizeInKBytes\": 2" +
" }\n" + " }\n" +
" },\n" + " },\n" +
" \"default\": false\n" + " \"default\": false\n" +
@ -172,7 +178,7 @@ public class TenantProfileController extends BaseController {
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.")
@RequestBody TenantProfile tenantProfile) throws ThingsboardException { @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException {
TenantProfile oldProfile; TenantProfile oldProfile;
if (tenantProfile.getId() == null) { if (tenantProfile.getId() == null) {
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE);

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

@ -30,6 +30,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
@ -51,6 +52,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
@ -200,7 +202,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
if (broadcast) { if (broadcast) {
broadcasts.add(link); broadcasts.add(link);
} else { } else {
TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, link.entityId()); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, link.tenantId(), link.entityId());
unicasts.computeIfAbsent(tpi, k -> new ArrayList<>()).add(link); unicasts.computeIfAbsent(tpi, k -> new ArrayList<>()).add(link);
} }
} }
@ -273,7 +275,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
long startTs = currentTime - timeWindow; long startTs = currentTime - timeWindow;
long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
int limit = argument.getLimit() == 0 ? (int) maxDataPoints : argument.getLimit(); int argumentLimit = argument.getLimit();
int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit();
ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE);
ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); ListenableFuture<List<TsKvEntry>> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));

8
application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java

@ -18,8 +18,10 @@ package org.thingsboard.server.service.cf.cache;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.QueueKey;
@ -57,7 +59,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override @Override
public void add(TenantId tenantId, EntityId profileId, EntityId entityId) { public void add(TenantId tenantId, EntityId profileId, EntityId entityId) {
var tpi = partitionService.resolve(QueueKey.CF, entityId); var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
var partition = tpi.getPartition().orElse(UNKNOWN); var partition = tpi.getPartition().orElse(UNKNOWN);
tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()) tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache())
.add(profileId, entityId, partition, tpi.isMyPartition()); .add(profileId, entityId, partition, tpi.isMyPartition());
@ -65,7 +67,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override @Override
public void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId) { public void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId) {
var tpi = partitionService.resolve(QueueKey.CF, entityId); var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
var partition = tpi.getPartition().orElse(UNKNOWN); var partition = tpi.getPartition().orElse(UNKNOWN);
var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()); var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache());
//TODO: make this method atomic; //TODO: make this method atomic;
@ -86,7 +88,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override @Override
public int getEntityIdPartition(TenantId tenantId, EntityId entityId) { public int getEntityIdPartition(TenantId tenantId, EntityId entityId) {
var tpi = partitionService.resolve(QueueKey.CF, entityId); var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
return tpi.getPartition().orElse(UNKNOWN); return tpi.getPartition().orElse(UNKNOWN);
} }

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

@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -44,7 +43,6 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Data @Data
public class CalculatedFieldCtx { public class CalculatedFieldCtx {
@ -58,8 +56,6 @@ public class CalculatedFieldCtx {
private final Map<String, Argument> arguments; private final Map<String, Argument> arguments;
private final Map<ReferencedEntityKey, String> mainEntityArguments; private final Map<ReferencedEntityKey, String> mainEntityArguments;
private final Map<EntityId, Map<ReferencedEntityKey, String>> linkedEntityArguments; private final Map<EntityId, Map<ReferencedEntityKey, String>> linkedEntityArguments;
private final Map<TbPair<EntityId, ReferencedEntityKey>, String> referencedEntityKeys;
private final List<String> argNames; private final List<String> argNames;
private Output output; private Output output;
private String expression; private String expression;
@ -93,11 +89,6 @@ public class CalculatedFieldCtx {
linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey());
} }
} }
this.referencedEntityKeys = arguments.entrySet().stream()
.collect(Collectors.toMap(
entry -> new TbPair<>(entry.getValue().getRefEntityId() == null ? entityId : entry.getValue().getRefEntityId(), entry.getValue().getRefEntityKey()),
Map.Entry::getKey
));
this.argNames = new ArrayList<>(arguments.keySet()); this.argNames = new ArrayList<>(arguments.keySet());
this.output = configuration.getOutput(); this.output = configuration.getOutput();
this.expression = configuration.getExpression(); this.expression = configuration.getExpression();
@ -136,11 +127,14 @@ public class CalculatedFieldCtx {
throw new IllegalArgumentException("TBEL script engine is disabled!"); throw new IllegalArgumentException("TBEL script engine is disabled!");
} }
List<String> ctxAndArgNames = new ArrayList<>(argNames.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(argNames);
return new CalculatedFieldTbelScriptEngine( return new CalculatedFieldTbelScriptEngine(
tenantId, tenantId,
tbelInvokeService, tbelInvokeService,
expression, expression,
argNames.toArray(String[]::new) ctxAndArgNames.toArray(String[]::new)
); );
} }

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

@ -20,10 +20,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
@ -67,9 +69,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
@Override @Override
public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) { public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) {
super.init(eventConsumer); super.init(eventConsumer);
var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME);
this.stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<CalculatedFieldStateProto>>create() this.stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<CalculatedFieldStateProto>>create()
.queueKey(QueueKey.CF_STATES) .queueKey(queueKey)
.topic(partitionService.getTopic(QueueKey.CF_STATES)) .topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval) .pollInterval(pollInterval)
.msgPackProcessor((msgs, consumer, config) -> { .msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<CalculatedFieldStateProto> msg : msgs) { for (TbProtoQueueMsg<CalculatedFieldStateProto> msg : msgs) {
@ -101,7 +105,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
@Override @Override
protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF_STATES, stateId.entityId()); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId());
TbProtoQueueMsg<CalculatedFieldStateProto> msg = new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto); TbProtoQueueMsg<CalculatedFieldStateProto> msg = new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto);
if (stateMsgProto == null) { if (stateMsgProto == null) {
putStateId(msg.getHeaders(), stateId); putStateId(msg.getHeaders(), stateId);

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

@ -23,11 +23,17 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Data @Data
@Slf4j @Slf4j
@ -49,11 +55,20 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
Object[] args = ctx.getArgNames().stream() Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
.map(this::toTbelArgument) List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
.toArray(); args.add(new Object()); // first element is a ctx, but we will set it later;
for (String argName : ctx.getArgNames()) {
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args); var arg = toTbelArgument(argName);
arguments.put(argName, arg);
if (arg instanceof TbelCfSingleValueArg svArg) {
args.add(svArg.getValue());
} else {
args.add(arg);
}
}
args.set(0, new TbelCfCtx(arguments));
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray());
Output output = ctx.getOutput(); Output output = ctx.getOutput();
return Futures.transform(resultFuture, return Futures.transform(resultFuture,
result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), result -> new CalculatedFieldResult(output.getType(), output.getScope(), result),

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

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

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

@ -27,6 +27,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.queue.QueueConfig;
@ -79,8 +80,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
private long pollInterval; private long pollInterval;
@Value("${queue.calculated_fields.pack_processing_timeout:60000}") @Value("${queue.calculated_fields.pack_processing_timeout:60000}")
private long packProcessingTimeout; private long packProcessingTimeout;
@Value("${queue.calculated_fields.pool_size:8}")
private int poolSize;
private final TbRuleEngineQueueFactory queueFactory; private final TbRuleEngineQueueFactory queueFactory;
private final CalculatedFieldStateService stateService; private final CalculatedFieldStateService stateService;
@ -108,9 +107,10 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
public void init() { public void init() {
super.init("tb-cf"); super.init("tb-cf");
var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME);
this.eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create() this.eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create()
.queueKey(QueueKey.CF) .queueKey(queueKey)
.topic(partitionService.getTopic(QueueKey.CF)) .topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval) .pollInterval(pollInterval)
.msgPackProcessor(this::processMsgs) .msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer())
@ -140,20 +140,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
// Cleanup old entities after corresponding consumers are stopped. // Cleanup old entities after corresponding consumers are stopped.
// Any periodic tasks need to check that the entity is still managed by the current server before processing. // Any periodic tasks need to check that the entity is still managed by the current server before processing.
actorContext.tell(new CalculatedFieldPartitionChangeMsg(partitionsToBooleanIndexArray(partitions))); actorContext.tell(new CalculatedFieldPartitionChangeMsg());
} catch (Throwable t) { } catch (Throwable t) {
log.error("Failed to process partition change event: {}", event, t); log.error("Failed to process partition change event: {}", event, t);
} }
} }
private boolean[] partitionsToBooleanIndexArray(Set<TopicPartitionInfo> partitions) {
boolean[] myPartitions = new boolean[partitionService.getTotalCalculatedFieldPartitions()];
for (var tpi : partitions) {
tpi.getPartition().ifPresent(partition -> myPartitions[partition] = true);
}
return myPartitions;
}
private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, QueueConfig config) throws Exception { private void processMsgs(List<TbProtoQueueMsg<ToCalculatedFieldMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> consumer, QueueConfig config) throws Exception {
List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
ConcurrentMap<UUID, TbProtoQueueMsg<ToCalculatedFieldMsg>> pendingMap = orderedMsgList.stream().collect( ConcurrentMap<UUID, TbProtoQueueMsg<ToCalculatedFieldMsg>> pendingMap = orderedMsgList.stream().collect(

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

@ -358,7 +358,7 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback) { public void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
pushMsgToCalculatedFields(tpi, UUID.randomUUID(), msg, callback); pushMsgToCalculatedFields(tpi, UUID.randomUUID(), msg, callback);
} }
@ -371,7 +371,7 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId);
producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback);
toRuleEngineNfs.incrementAndGet(); toRuleEngineNfs.incrementAndGet();
} }

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

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

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

@ -22,6 +22,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -108,7 +109,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
event.getNewPartitions().forEach((queueKey, partitions) -> { event.getNewPartitions().forEach((queueKey, partitions) -> {
if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) { if (DataConstants.CF_QUEUE_NAME.equals(queueKey.getQueueName()) || DataConstants.CF_STATES_QUEUE_NAME.equals(queueKey.getQueueName())) {
return; return;
} }
if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) { if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) {

8
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -661,16 +661,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
List<TsKvEntry> activityTimeseries = Futures.getDone(timeseriesActivityDataFuture); List<TsKvEntry> activityTimeseries = Futures.getDone(timeseriesActivityDataFuture);
Optional<AttributeKvEntry> inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture); Optional<AttributeKvEntry> inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture);
List<KvEntry> result;
if (inactivityTimeoutAttribute.isPresent()) { if (inactivityTimeoutAttribute.isPresent()) {
result = new ArrayList<>(activityTimeseries.size() + 1); List<KvEntry> result = new ArrayList<>(activityTimeseries.size() + 1);
result.addAll(activityTimeseries); result.addAll(activityTimeseries);
inactivityTimeoutAttribute.ifPresent(result::add); result.add(inactivityTimeoutAttribute.get());
return result;
} else { } else {
return activityTimeseries; return activityTimeseries;
} }
return result;
}, deviceStateCallbackExecutor); }, deviceStateCallbackExecutor);
future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor()); future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor());

83
application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java

@ -1,83 +0,0 @@
/**
* 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.stats;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.JsInvokeStats;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
@Service
public class DefaultJsInvokeStats implements JsInvokeStats {
private static final String REQUESTS = "requests";
private static final String RESPONSES = "responses";
private static final String FAILURES = "failures";
private StatsCounter requestsCounter;
private StatsCounter responsesCounter;
private StatsCounter failuresCounter;
@Autowired
private StatsFactory statsFactory;
@PostConstruct
public void init() {
String key = StatsType.JS_INVOKE.getName();
this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS);
this.responsesCounter = statsFactory.createStatsCounter(key, RESPONSES);
this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES);
}
@Override
public void incrementRequests(int amount) {
requestsCounter.add(amount);
}
@Override
public void incrementResponses(int amount) {
responsesCounter.add(amount);
}
@Override
public void incrementFailures(int amount) {
failuresCounter.add(amount);
}
@Override
public int getRequests() {
return requestsCounter.get();
}
@Override
public int getResponses() {
return responsesCounter.get();
}
@Override
public int getFailures() {
return failuresCounter.get();
}
@Override
public void reset() {
requestsCounter.clear();
responsesCounter.clear();
failuresCounter.clear();
}
}

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

@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -49,8 +48,6 @@ import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
@ -76,7 +73,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
private final TbServiceInfoProvider serviceInfoProvider; private final TbServiceInfoProvider serviceInfoProvider;
private final TbQueueProducerProvider producerProvider; private final TbQueueProducerProvider producerProvider;
private final TbLocalSubscriptionService localSubscriptionService; private final TbLocalSubscriptionService localSubscriptionService;
private final DeviceStateService deviceStateService;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final SubscriptionSchedulerComponent scheduler; private final SubscriptionSchedulerComponent scheduler;
@ -171,7 +167,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
callback.onSuccess(); callback.onSuccess();
} }
public void onTimeSeriesUpdate(EntityId entityId, List<TsKvEntry> update) { private void onTimeSeriesUpdate(EntityId entityId, List<TsKvEntry> update) {
getEntityUpdatesInfo(entityId).timeSeriesUpdateTs = System.currentTimeMillis(); getEntityUpdatesInfo(entityId).timeSeriesUpdateTs = System.currentTimeMillis();
TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId);
if (subInfo != null) { if (subInfo != null) {
@ -201,42 +197,27 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
@Override @Override
public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbCallback callback) { public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbCallback callback) {
onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback);
}
@Override
public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, TbCallback callback) {
getEntityUpdatesInfo(entityId).attributesUpdateTs = System.currentTimeMillis(); getEntityUpdatesInfo(entityId).attributesUpdateTs = System.currentTimeMillis();
processAttributesUpdate(entityId, scope, attributes); processAttributesUpdate(entityId, scope, attributes);
if (entityId.getEntityType() == EntityType.DEVICE) {
if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) {
updateDeviceInactivityTimeout(tenantId, entityId, attributes);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
, null);
}
}
callback.onSuccess(); callback.onSuccess();
} }
@Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, TbCallback callback) {
onAttributesDelete(tenantId, entityId, scope, keys, false, callback);
}
@Override @Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback callback) { public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback callback) {
processAttributesUpdate(entityId, scope, processAttributesUpdate(entityId, scope,
keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList()));
if (entityId.getEntityType() == EntityType.DEVICE) { if (entityId.getEntityType() == EntityType.DEVICE && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope) clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), scope, keys), null);
|| TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) {
deleteDeviceInactivityTimeout(tenantId, entityId, keys);
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId,
new DeviceId(entityId.getId()), scope, keys), null);
}
} }
callback.onSuccess(); callback.onSuccess();
} }
public void processAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> update) { private void processAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> update) {
TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId);
if (subInfo != null) { if (subInfo != null) {
log.trace("[{}] Handling attributes update: {}", entityId, update); log.trace("[{}] Handling attributes update: {}", entityId, update);
@ -264,22 +245,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
} }
} }
private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<? extends KvEntry> kvEntries) {
for (KvEntry kvEntry : kvEntries) {
if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), getLongValue(kvEntry));
}
}
}
private void deleteDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<String> keys) {
for (String key : keys) {
if (key.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) {
deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), 0);
}
}
}
@Override @Override
public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) {
onAlarmSubUpdate(tenantId, entityId, alarm, false, callback); onAlarmSubUpdate(tenantId, entityId, alarm, false, callback);
@ -349,29 +314,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
} }
} }
private static long getLongValue(KvEntry kve) {
switch (kve.getDataType()) {
case LONG:
return kve.getLongValue().orElse(0L);
case DOUBLE:
return kve.getDoubleValue().orElse(0.0).longValue();
case STRING:
try {
return Long.parseLong(kve.getStrValue().orElse("0"));
} catch (NumberFormatException e) {
return 0L;
}
case JSON:
try {
return Long.parseLong(kve.getJsonValue().orElse("0"));
} catch (NumberFormatException e) {
return 0L;
}
default:
return 0L;
}
}
private static <T extends KvEntry> List<T> getSubList(List<T> ts, Set<String> keys) { private static <T extends KvEntry> List<T> getSubList(List<T> ts, Set<String> keys) {
List<T> update = null; List<T> update = null;
for (T entry : ts) { for (T entry : ts) {

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

@ -39,8 +39,15 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbCallback callback); void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbCallback callback);
void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, TbCallback callback); 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 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 onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List<String> keys, TbCallback callback);

3
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java

@ -209,7 +209,7 @@ public class TbSubscriptionUtils {
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
} }
public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice) { public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List<String> keys) {
TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder(); TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder();
builder.setEntityType(entityId.getEntityType().name()); builder.setEntityType(entityId.getEntityType().name());
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits());
@ -218,7 +218,6 @@ public class TbSubscriptionUtils {
builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
builder.setScope(scope); builder.setScope(scope);
builder.addAllKeys(keys); builder.addAllKeys(keys);
builder.setNotifyDevice(notifyDevice);
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder();
msgBuilder.setAttrDelete(builder); msgBuilder.setAttrDelete(builder);

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

@ -529,6 +529,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) { public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) {
log.debug("[{}] Deleting version control settings", tenantId);
repositorySettingsService.delete(tenantId);
return gitServiceQueue.clearRepository(tenantId); return gitServiceQueue.clearRepository(tenantId);
} }

3
application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java

@ -111,8 +111,7 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {}
}
}, executor); }, executor);
} }

143
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -31,20 +31,26 @@ import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.CustomerId; 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -52,10 +58,11 @@ import org.thingsboard.server.dao.util.KvUtils;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -65,6 +72,11 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.function.Consumer; import java.util.function.Consumer;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingLong;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
/** /**
* Created by ashvayka on 27.03.18. * Created by ashvayka on 27.03.18.
*/ */
@ -78,6 +90,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
private final CalculatedFieldQueueService calculatedFieldQueueService; private final CalculatedFieldQueueService calculatedFieldQueueService;
private final DeviceStateManager deviceStateManager;
private ExecutorService tsCallBackExecutor; private ExecutorService tsCallBackExecutor;
@ -89,13 +102,15 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Lazy TbEntityViewService tbEntityViewService, @Lazy TbEntityViewService tbEntityViewService,
TbApiUsageReportClient apiUsageClient, TbApiUsageReportClient apiUsageClient,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
CalculatedFieldQueueService calculatedFieldQueueService) { CalculatedFieldQueueService calculatedFieldQueueService,
DeviceStateManager deviceStateManager) {
this.attrService = attrService; this.attrService = attrService;
this.tsService = tsService; this.tsService = tsService;
this.tbEntityViewService = tbEntityViewService; this.tbEntityViewService = tbEntityViewService;
this.apiUsageClient = apiUsageClient; this.apiUsageClient = apiUsageClient;
this.apiUsageStateService = apiUsageStateService; this.apiUsageStateService = apiUsageStateService;
this.calculatedFieldQueueService = calculatedFieldQueueService; this.calculatedFieldQueueService = calculatedFieldQueueService;
this.deviceStateManager = deviceStateManager;
} }
@PostConstruct @PostConstruct
@ -140,6 +155,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
EntityId entityId = request.getEntityId(); EntityId entityId = request.getEntityId();
TimeseriesSaveRequest.Strategy strategy = request.getStrategy(); TimeseriesSaveRequest.Strategy strategy = request.getStrategy();
ListenableFuture<TimeseriesSaveResult> resultFuture; ListenableFuture<TimeseriesSaveResult> resultFuture;
if (strategy.saveTimeseries() && strategy.saveLatest()) { if (strategy.saveTimeseries() && strategy.saveLatest()) {
resultFuture = tsService.save(tenantId, entityId, request.getEntries(), request.getTtl()); resultFuture = tsService.save(tenantId, entityId, request.getEntries(), request.getTtl());
} else if (strategy.saveLatest()) { } else if (strategy.saveLatest()) {
@ -176,11 +192,68 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override @Override
public void saveAttributesInternal(AttributesSaveRequest request) { public void saveAttributesInternal(AttributesSaveRequest request) {
log.trace("Executing saveInternal [{}]", request); log.trace("Executing saveInternal [{}]", request);
ListenableFuture<List<Long>> saveFuture = attrService.save(request.getTenantId(), request.getEntityId(), request.getScope(), request.getEntries()); TenantId tenantId = request.getTenantId();
DonAsynchron.withCallback(saveFuture, result -> { EntityId entityId = request.getEntityId();
calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()); AttributesSaveRequest.Strategy strategy = request.getStrategy();
}, safeCallback(request.getCallback()), tsCallBackExecutor); ListenableFuture<List<Long>> resultFuture;
addWsCallback(saveFuture, success -> onAttributesUpdate(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getEntries(), request.isNotifyDevice()));
if (strategy.saveAttributes()) {
resultFuture = attrService.save(tenantId, entityId, request.getScope(), request.getEntries());
} else {
resultFuture = Futures.immediateFuture(Collections.emptyList());
}
addMainCallback(resultFuture, result -> {
if (strategy.processCalculatedFields()) {
calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback());
} else {
request.getCallback().onSuccess(null);
}
}, t -> request.getCallback().onFailure(t));
if (shouldSendSharedAttributesUpdatedNotification(request)) {
addMainCallback(resultFuture, success -> clusterService.pushMsgToCore(
DeviceAttributesEventNotificationMsg.onUpdate(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getEntries()), null
));
}
if (shouldCheckForInactivityTimeoutUpdates(request)) {
findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout ->
addMainCallback(resultFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate(
tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY)
)
);
}
if (strategy.sendWsUpdate()) {
addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries()));
}
}
private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) {
return request.getStrategy().saveAttributes() && shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice());
}
private static boolean shouldCheckForInactivityTimeoutUpdates(AttributesSaveRequest request) {
return request.getStrategy().saveAttributes()
&& request.getEntityId().getEntityType() == EntityType.DEVICE
&& request.getScope() == AttributeScope.SERVER_SCOPE;
}
private static Optional<Long> findNewInactivityTimeout(List<AttributeKvEntry> entries) {
return entries.stream()
.filter(entry -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, entry.getKey()))
// Select the entry with the highest version, or if the versions are equal, the one with the most recent update timestamp
.max(comparing(AttributeKvEntry::getVersion, nullsFirst(naturalOrder())).thenComparingLong(AttributeKvEntry::getLastUpdateTs))
.map(DefaultTelemetrySubscriptionService::parseAsLong);
}
private static long parseAsLong(KvEntry kve) {
try {
return Long.parseLong(kve.getValueAsString());
} catch (NumberFormatException e) {
return 0L;
}
} }
@Override @Override
@ -191,11 +264,45 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override @Override
public void deleteAttributesInternal(AttributesDeleteRequest request) { public void deleteAttributesInternal(AttributesDeleteRequest request) {
ListenableFuture<List<String>> deleteFuture = attrService.removeAll(request.getTenantId(), request.getEntityId(), request.getScope(), request.getKeys()); TenantId tenantId = request.getTenantId();
DonAsynchron.withCallback(deleteFuture, result -> { EntityId entityId = request.getEntityId();
calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback());
}, safeCallback(request.getCallback()), tsCallBackExecutor); ListenableFuture<List<String>> deleteFuture = attrService.removeAll(tenantId, entityId, request.getScope(), request.getKeys());
addWsCallback(deleteFuture, success -> onAttributesDelete(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getKeys(), request.isNotifyDevice()));
addMainCallback(deleteFuture,
result -> calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()),
t -> request.getCallback().onFailure(t)
);
if (shouldSendSharedAttributesDeletedNotification(request)) {
addMainCallback(deleteFuture, success -> clusterService.pushMsgToCore(
DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getKeys()), null
));
}
if (inactivityTimeoutDeleted(request)) {
addMainCallback(deleteFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate(
tenantId, new DeviceId(entityId.getId()), 0L, TbCallback.EMPTY)
);
}
addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, request.getScope().name(), request.getKeys()));
}
private static boolean shouldSendSharedAttributesDeletedNotification(AttributesDeleteRequest request) {
return shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice());
}
private static boolean shouldSendSharedAttributesNotification(EntityId entityId, AttributeScope scope, boolean notifyDevice) {
return entityId.getEntityType() == EntityType.DEVICE
&& scope == AttributeScope.SHARED_SCOPE
&& notifyDevice;
}
private static boolean inactivityTimeoutDeleted(AttributesDeleteRequest request) {
return request.getEntityId().getEntityType() == EntityType.DEVICE
&& request.getScope() == AttributeScope.SERVER_SCOPE
&& request.getKeys().stream().anyMatch(key -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, key));
} }
@Override @Override
@ -247,7 +354,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
if (entries != null) { if (entries != null) {
Optional<TsKvEntry> tsKvEntry = entries.stream() Optional<TsKvEntry> tsKvEntry = entries.stream()
.filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs)
.max(Comparator.comparingLong(TsKvEntry::getTs)); .max(comparingLong(TsKvEntry::getTs));
tsKvEntry.ifPresent(entityViewLatest::add); tsKvEntry.ifPresent(entityViewLatest::add);
} }
} }
@ -280,16 +387,16 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
} }
} }
private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice) { private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
forwardToSubscriptionManagerService(tenantId, entityId, forwardToSubscriptionManagerService(tenantId, entityId,
subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY), subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY),
() -> TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes)); () -> TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes));
} }
private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice) { private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys) {
forwardToSubscriptionManagerService(tenantId, entityId, forwardToSubscriptionManagerService(tenantId, entityId,
subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY), subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, TbCallback.EMPTY),
() -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice)); () -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys));
} }
private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts) { private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts) {

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

@ -502,8 +502,6 @@ actors:
statistics: statistics:
# Enable/disable actor statistics # Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED:true}" enabled: "${ACTORS_STATISTICS_ENABLED:true}"
# Frequency of printing the JS executor statistics
js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}"
# Actors statistic persistence frequency in milliseconds # Actors statistic persistence frequency in milliseconds
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
calculated_fields: calculated_fields:
@ -817,7 +815,7 @@ spring:
events: events:
# Enable dedicated datasource (a separate database) for events and audit logs. # Enable dedicated datasource (a separate database) for events and audit logs.
# Before enabling this, make sure you have set up the following tables in the new DB: # Before enabling this, make sure you have set up the following tables in the new DB:
# error_event, lc_event, rule_chain_debug_event, rule_node_debug_event, stats_event, audit_log # error_event, lc_event, rule_chain_debug_event, rule_node_debug_event, stats_event, audit_log, cf_debug_event
enabled: "${SPRING_DEDICATED_EVENTS_DATASOURCE_ENABLED:false}" enabled: "${SPRING_DEDICATED_EVENTS_DATASOURCE_ENABLED:false}"
# Database driver for Spring JPA for events datasource # Database driver for Spring JPA for events datasource
driverClassName: "${SPRING_EVENTS_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}" driverClassName: "${SPRING_EVENTS_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}"
@ -858,6 +856,7 @@ audit-log:
"edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
"calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation
sink: sink:
# Type of external sink. possible options: none, elasticsearch # Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}" type: "${AUDIT_LOG_SINK_TYPE:none}"
@ -1283,7 +1282,7 @@ transport:
# URL of gateways dashboard repository # URL of gateways dashboard repository
repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}" repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}"
# Branch of gateways dashboard repository to work with # Branch of gateways dashboard repository to work with
branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:}" branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}"
# Fetch frequency in hours for gateways dashboard repository # Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
@ -1758,9 +1757,6 @@ queue:
stats: stats:
# Enable/disable statistics for EDQS # Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}" enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Statistics printing interval for EDQS
print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}"
vc: vc:
# Default topic name # Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"
@ -1817,9 +1813,7 @@ queue:
# For high-priority notifications that require minimum latency and processing time # For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}" notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}"
# Interval in milliseconds to poll messages by CF (Rule Engine) microservices # Interval in milliseconds to poll messages by CF (Rule Engine) microservices
poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:1000}"
# Amount of partitions used by CF microservices
partitions: "${TB_QUEUE_CF_PARTITIONS:10}"
# Timeout for processing a message pack by CF microservices # Timeout for processing a message pack by CF microservices
pack_processing_timeout: "${TB_QUEUE_CF_PACK_PROCESSING_TIMEOUT_MS:60000}" pack_processing_timeout: "${TB_QUEUE_CF_PACK_PROCESSING_TIMEOUT_MS:60000}"
# Thread pool size for processing of the incoming messages # Thread pool size for processing of the incoming messages

79
application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java

@ -0,0 +1,79 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class RepositorySettingsTest extends AbstractControllerTest {
@MockBean
private GitVersionControlQueueService gitVersionControlQueueService;
@Test
public void testFindRepositorySettings() throws Exception {
loginTenantAdmin();
doGet("/api/admin/repositorySettings")
.andExpect(status().isNotFound());
String testRepositoryUri = "https://github.com/test/version-control-test-repository.git";
SettableFuture<Void> successFuture = SettableFuture.create();
successFuture.set(null);
when(gitVersionControlQueueService.initRepository(any(), any()))
.thenReturn(successFuture);
RepositorySettings repositorySettings = new RepositorySettings();
repositorySettings.setPassword("test");
repositorySettings.setAuthMethod(RepositoryAuthMethod.USERNAME_PASSWORD);
repositorySettings.setRepositoryUri(testRepositoryUri);
repositorySettings.setDefaultBranch("main");
doPost("/api/admin/repositorySettings", repositorySettings)
.andExpect(status().isOk());
// check repository settings
doGet("/api/admin/repositorySettings")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.repositoryUri", is(testRepositoryUri)));
// delete settings
when(gitVersionControlQueueService.clearRepository(any()))
.thenReturn(successFuture);
doDelete("/api/admin/repositorySettings")
.andExpect(status().isOk());
// check repository settings
doGet("/api/admin/repositorySettings")
.andExpect(status().isNotFound());
}
}

6
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.CalculatedFieldResult;
@ -51,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@SpringBootTest(classes = DefaultTbelInvokeService.class) @SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class})
public class ScriptCalculatedFieldStateTest { public class ScriptCalculatedFieldStateTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382")); private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382"));
@ -191,7 +193,7 @@ public class ScriptCalculatedFieldStateTest {
config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2));
config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity.value}"); config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}");
Output output = new Output(); Output output = new Output();
output.setType(OutputType.ATTRIBUTES); output.setType(OutputType.ATTRIBUTES);

5
application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -22,7 +23,7 @@ import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.common.stats.DefaultStatsFactory;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -30,7 +31,7 @@ import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@SpringBootTest(classes = DefaultTbelInvokeService.class) @SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class })
public abstract class AbstractTbelInvokeTest { public abstract class AbstractTbelInvokeTest {
@Autowired @Autowired

8
application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java

@ -21,9 +21,13 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.js.JsInvokeProtos;
@ -68,6 +72,10 @@ class RemoteJsInvokeServiceTest {
remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient)); remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient));
jsRequestTemplate = mock(TbQueueRequestTemplate.class); jsRequestTemplate = mock(TbQueueRequestTemplate.class);
remoteJsInvokeService.requestTemplate = jsRequestTemplate; remoteJsInvokeService.requestTemplate = jsRequestTemplate;
StatsFactory statsFactory = mock(StatsFactory.class);
when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class));
ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory);
remoteJsInvokeService.init();
} }
@AfterEach @AfterEach

6
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -69,7 +69,7 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -1074,7 +1074,7 @@ public class DefaultDeviceStateServiceTest {
final long defaultTimeout = 1000; final long defaultTimeout = 1000;
initStateService(defaultTimeout); initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@ -1156,7 +1156,7 @@ public class DefaultDeviceStateServiceTest {
final long defaultTimeout = 1000; final long defaultTimeout = 1000;
initStateService(defaultTimeout); initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()

759
application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java

@ -24,25 +24,36 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.AttributesEntityView;
@ -50,6 +61,7 @@ import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -73,14 +85,17 @@ import java.util.concurrent.ExecutorService;
import java.util.stream.LongStream; import java.util.stream.LongStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateFuture; import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class DefaultTelemetrySubscriptionServiceTest { class DefaultTelemetrySubscriptionServiceTest {
@ -91,7 +106,7 @@ class DefaultTelemetrySubscriptionServiceTest {
final long sampleTtl = 10_000L; final long sampleTtl = 10_000L;
final List<TsKvEntry> sampleTelemetry = List.of( final List<TsKvEntry> sampleTimeseries = List.of(
new BasicTsKvEntry(100L, new DoubleDataEntry("temperature", 65.2)), new BasicTsKvEntry(100L, new DoubleDataEntry("temperature", 65.2)),
new BasicTsKvEntry(100L, new DoubleDataEntry("humidity", 33.1)) new BasicTsKvEntry(100L, new DoubleDataEntry("humidity", 33.1))
); );
@ -124,12 +139,14 @@ class DefaultTelemetrySubscriptionServiceTest {
TbApiUsageStateService apiUsageStateService; TbApiUsageStateService apiUsageStateService;
@Mock @Mock
CalculatedFieldQueueService calculatedFieldQueueService; CalculatedFieldQueueService calculatedFieldQueueService;
@Mock
DeviceStateManager deviceStateManager;
DefaultTelemetrySubscriptionService telemetryService; DefaultTelemetrySubscriptionService telemetryService;
@BeforeEach @BeforeEach
void setup() { void setup() {
telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService); telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService, deviceStateManager);
ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService); ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService);
ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService); ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService);
ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService)); ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService));
@ -146,9 +163,9 @@ class DefaultTelemetrySubscriptionServiceTest {
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi);
lenient().when(tsService.save(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), null))); lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null)));
lenient().when(tsService.saveLatest(tenantId, entityId, sampleTelemetry)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); lenient().when(tsService.saveLatest(tenantId, entityId, sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock no entity views // mock no entity views
lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(Collections.emptyList())); lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(Collections.emptyList()));
@ -179,7 +196,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(new ApiUsageStateId(UUID.randomUUID())) .entityId(new ApiUsageStateId(UUID.randomUUID()))
.entries(sampleTelemetry) .entries(sampleTimeseries)
.strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL)
.build(); .build();
@ -199,7 +216,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false)) .strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false))
.build(); .build();
@ -208,7 +225,7 @@ class DefaultTelemetrySubscriptionServiceTest {
telemetryService.saveTimeseries(request); telemetryService.saveTimeseries(request);
// THEN // THEN
then(apiUsageClient).should().report(tenantId, customerId, ApiUsageRecordKey.STORAGE_DP_COUNT, sampleTelemetry.size()); then(apiUsageClient).should().report(tenantId, customerId, ApiUsageRecordKey.STORAGE_DP_COUNT, sampleTimeseries.size());
} }
@Test @Test
@ -218,7 +235,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.build(); .build();
@ -240,7 +257,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL)
.future(future) .future(future)
@ -266,7 +283,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.future(future) .future(future)
@ -286,12 +303,12 @@ class DefaultTelemetrySubscriptionServiceTest {
entityView.setTenantId(tenantId); entityView.setTenantId(tenantId);
entityView.setCustomerId(customerId); entityView.setCustomerId(customerId);
entityView.setEntityId(entityId); entityView.setEntityId(entityId);
entityView.setKeys(new TelemetryEntityView(sampleTelemetry.stream().map(KvEntry::getKey).toList(), new AttributesEntityView())); entityView.setKeys(new TelemetryEntityView(sampleTimeseries.stream().map(KvEntry::getKey).toList(), new AttributesEntityView()));
// mock that there is one entity view // mock that there is one entity view
given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView)));
// mock that save latest call for entity view is successful // mock that save latest call for entity view is successful
given(tsService.saveLatest(tenantId, entityView.getId(), sampleTelemetry)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size()))));
// mock TPI for entity view // mock TPI for entity view
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi); given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi);
@ -299,7 +316,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false))
.build(); .build();
@ -309,12 +326,12 @@ class DefaultTelemetrySubscriptionServiceTest {
// THEN // THEN
// should save latest to both the main entity and it's entity view // should save latest to both the main entity and it's entity view
then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry); then(tsService).should().saveLatest(tenantId, entityId, sampleTimeseries);
then(tsService).should().saveLatest(tenantId, entityView.getId(), sampleTelemetry); then(tsService).should().saveLatest(tenantId, entityView.getId(), sampleTimeseries);
then(tsService).shouldHaveNoMoreInteractions(); then(tsService).shouldHaveNoMoreInteractions();
// should send WS update only for entity view (WS update for the main entity is disabled in the save request) // should send WS update only for entity view (WS update for the main entity is disabled in the save request)
then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityView.getId(), sampleTelemetry, TbCallback.EMPTY); then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityView.getId(), sampleTimeseries, TbCallback.EMPTY);
then(subscriptionManagerService).shouldHaveNoMoreInteractions(); then(subscriptionManagerService).shouldHaveNoMoreInteractions();
} }
@ -325,7 +342,7 @@ class DefaultTelemetrySubscriptionServiceTest {
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false)) .strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false))
.build(); .build();
@ -335,7 +352,7 @@ class DefaultTelemetrySubscriptionServiceTest {
// THEN // THEN
// should save only time series for the main entity // should save only time series for the main entity
then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl); then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl);
then(tsService).shouldHaveNoMoreInteractions(); then(tsService).shouldHaveNoMoreInteractions();
// should not send any WS updates // should not send any WS updates
@ -343,14 +360,14 @@ class DefaultTelemetrySubscriptionServiceTest {
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("booleanCombinations") @MethodSource("allCombinationsOfFourBooleans")
void shouldCallCorrectApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate, boolean processCalculatedFields) { void shouldCallCorrectSaveTimeseriesApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate, boolean processCalculatedFields) {
// GIVEN // GIVEN
var request = TimeseriesSaveRequest.builder() var request = TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.customerId(customerId) .customerId(customerId)
.entityId(entityId) .entityId(entityId)
.entries(sampleTelemetry) .entries(sampleTimeseries)
.ttl(sampleTtl) .ttl(sampleTtl)
.strategy(new TimeseriesSaveRequest.Strategy(saveTimeseries, saveLatest, sendWsUpdate, processCalculatedFields)) .strategy(new TimeseriesSaveRequest.Strategy(saveTimeseries, saveLatest, sendWsUpdate, processCalculatedFields))
.build(); .build();
@ -360,11 +377,11 @@ class DefaultTelemetrySubscriptionServiceTest {
// THEN // THEN
if (saveTimeseries && saveLatest) { if (saveTimeseries && saveLatest) {
then(tsService).should().save(tenantId, entityId, sampleTelemetry, sampleTtl); then(tsService).should().save(tenantId, entityId, sampleTimeseries, sampleTtl);
} else if (saveLatest) { } else if (saveLatest) {
then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry); then(tsService).should().saveLatest(tenantId, entityId, sampleTimeseries);
} else if (saveTimeseries) { } else if (saveTimeseries) {
then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl); then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl);
} }
if (processCalculatedFields) { if (processCalculatedFields) {
@ -374,13 +391,13 @@ class DefaultTelemetrySubscriptionServiceTest {
then(tsService).shouldHaveNoMoreInteractions(); then(tsService).shouldHaveNoMoreInteractions();
if (sendWsUpdate) { if (sendWsUpdate) {
then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityId, sampleTelemetry, TbCallback.EMPTY); then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityId, sampleTimeseries, TbCallback.EMPTY);
} else { } else {
then(subscriptionManagerService).shouldHaveNoInteractions(); then(subscriptionManagerService).shouldHaveNoInteractions();
} }
} }
private static Stream<Arguments> booleanCombinations() { private static Stream<Arguments> allCombinationsOfFourBooleans() {
return Stream.of( return Stream.of(
Arguments.of(true, true, true, true), Arguments.of(true, true, true, true),
Arguments.of(true, true, true, false), Arguments.of(true, true, true, false),
@ -401,7 +418,693 @@ class DefaultTelemetrySubscriptionServiceTest {
); );
} }
// used to emulate sequence numbers returned by save latest API /* --- Save attributes API --- */
@ParameterizedTest
@MethodSource("allCombinationsOfThreeBooleans")
void shouldCallCorrectSaveAttributesApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveAttributes, boolean sendWsUpdate, boolean processCalculatedFields) {
// GIVEN
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(entityId)
.scope(AttributeScope.SERVER_SCOPE)
.entry(new DoubleDataEntry("temperature", 65.2))
.notifyDevice(false)
.strategy(new AttributesSaveRequest.Strategy(saveAttributes, sendWsUpdate, processCalculatedFields))
.build();
lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries())).thenReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
if (saveAttributes) {
then(attrService).should().save(tenantId, entityId, request.getScope(), request.getEntries());
} else {
then(attrService).shouldHaveNoInteractions();
}
if (processCalculatedFields) {
then(calculatedFieldQueueService).should().pushRequestToQueue(eq(request), any(), eq(request.getCallback()));
}
if (sendWsUpdate) {
then(subscriptionManagerService).should().onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), TbCallback.EMPTY);
} else {
then(subscriptionManagerService).shouldHaveNoInteractions();
}
}
static Stream<Arguments> allCombinationsOfThreeBooleans() {
return Stream.of(
Arguments.of(true, true, true),
Arguments.of(true, true, false),
Arguments.of(true, false, true),
Arguments.of(true, false, false),
Arguments.of(false, true, true),
Arguments.of(false, true, false),
Arguments.of(false, false, true),
Arguments.of(false, false, false)
);
}
@Test
void shouldThrowErrorWhenTryingToSaveAttributesForApiUsageState() {
// GIVEN
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(new ApiUsageStateId(UUID.randomUUID()))
.scope(AttributeScope.SHARED_SCOPE)
.entry(new DoubleDataEntry("temperature", 65.2))
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
// WHEN
assertThatThrownBy(() -> telemetryService.saveAttributes(request))
.isInstanceOf(RuntimeException.class)
.hasMessage("Can't update API Usage State!");
// THEN
then(attrService).shouldHaveNoInteractions();
}
@Test
void shouldSendAttributesUpdateNotificationWhenDeviceSharedAttributesAreSavedAndNotifyDeviceIsTrue() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.entries(entries)
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
var expectedAttributesUpdateMsg = DeviceAttributesEventNotificationMsg.onUpdate(tenantId, deviceId, "SHARED_SCOPE", entries);
then(clusterService).should().pushMsgToCore(eq(expectedAttributesUpdateMsg), isNull());
}
@ParameterizedTest
@EnumSource(
value = EntityType.class,
names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotSendAttributesUpdateNotificationWhenEntityIsNotDevice(EntityType entityType) {
// GIVEN
var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(nonDeviceId)
.scope(AttributeScope.SHARED_SCOPE)
.entries(entries)
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@ParameterizedTest
@EnumSource(
value = AttributeScope.class,
names = "SHARED_SCOPE",
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotSendAttributesUpdateNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(notSharedScope)
.entries(entries)
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotSendAttributesUpdateNotificationWhenNotifyDeviceIsFalse() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.entries(entries)
.notifyDevice(false)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveWasSkipped() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.entries(entries)
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(false, false, false))
.build();
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveFailed() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)),
new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test"))
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.entries(entries)
.notifyDevice(true)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFailedFuture(new RuntimeException("failed to save")));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasUpdated() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L));
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entry(inactivityTimeout)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 5000L, TbCallback.EMPTY);
}
@Test
void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutSaveWasSkipped() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L));
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entry(inactivityTimeout)
.strategy(new AttributesSaveRequest.Strategy(false, true, true))
.build();
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@ParameterizedTest
@EnumSource(
value = EntityType.class,
names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButEntityTypeIsNotDevice(EntityType entityType) {
// GIVEN
var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088");
var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L));
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(nonDeviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entry(inactivityTimeout)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@ParameterizedTest
@EnumSource(
value = AttributeScope.class,
names = {"SERVER_SCOPE"},
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButAttributeScopeIsNotServer(AttributeScope nonServerScope) {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L));
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(nonServerScope)
.entry(inactivityTimeout)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@Test
void shouldNotNotifyDeviceStateManagerWhenUpdatedAttributesDoNotContainInactivityTimeout() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("notInactivityTimeout", 5000L));
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entry(inactivityTimeout)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@Test
void shouldUseInactivityTimeoutEntryWithTheGreatestVersion() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 0L), 0L, null),
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 3L, 1L),
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 2L),
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 1L, 3L)
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entries(entries)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY);
}
@Test
void shouldUseInactivityTimeoutEntryWithTheGreatestLastUpdateTsWhenVersionsAreTheSame() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<AttributeKvEntry> entries = List.of(
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 1L, 1L),
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 1L),
new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 3L, 1L)
);
var request = AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.entries(entries)
.strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size())));
// WHEN
telemetryService.saveAttributes(request);
// THEN
then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY);
}
/* --- Delete attributes API --- */
@Test
void shouldThrowErrorWhenTryingToDeleteAttributesForApiUsageState() {
// GIVEN
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(new ApiUsageStateId(UUID.randomUUID()))
.scope(AttributeScope.SHARED_SCOPE)
.keys(List.of("attributeKeyToDelete1", "attributeKeyToDelete2"))
.notifyDevice(true)
.build();
// WHEN
assertThatThrownBy(() -> telemetryService.deleteAttributes(request))
.isInstanceOf(RuntimeException.class)
.hasMessage("Can't update API Usage State!");
// THEN
then(attrService).shouldHaveNoInteractions();
}
@Test
void shouldSendAttributesDeletedNotificationWhenDeviceSharedAttributesAreDeletedAndNotifyDeviceIsTrue() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<String> keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.keys(keys)
.notifyDevice(true)
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
var expectedAttributesDeletedMsg = DeviceAttributesEventNotificationMsg.onDelete(tenantId, deviceId, "SHARED_SCOPE", List.of("attributeKeyToDelete1", "attributeKeyToDelete2"));
then(clusterService).should().pushMsgToCore(eq(expectedAttributesDeletedMsg), isNull());
}
@ParameterizedTest
@EnumSource(
value = EntityType.class,
names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotSendAttributesDeletedNotificationWhenEntityIsNotDevice(EntityType entityType) {
// GIVEN
var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088");
List<String> keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(nonDeviceId)
.scope(AttributeScope.SHARED_SCOPE)
.keys(keys)
.notifyDevice(true)
.build();
given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), keys)).willReturn(immediateFuture(keys));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@ParameterizedTest
@EnumSource(
value = AttributeScope.class,
names = "SHARED_SCOPE",
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotSendAttributesDeletedNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<String> keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(notSharedScope)
.keys(keys)
.notifyDevice(true)
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotSendAttributesDeletedNotificationWhenNotifyDeviceIsFalse() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<String> keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.keys(keys)
.notifyDevice(false)
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotSendAttributesDeletedNotificationWhenAttributesDeleteFailed() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
List<String> keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SHARED_SCOPE)
.keys(keys)
.notifyDevice(true)
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFailedFuture(new RuntimeException("failed to delete")));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(clusterService).should(never()).pushMsgToCore(any(), any());
}
@Test
void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasDeleted() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.keys(List.of("inactivityTimeout", "someOtherDeletedAttribute"))
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys()));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 0L, TbCallback.EMPTY);
}
@ParameterizedTest
@EnumSource(
value = EntityType.class,
names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButEntityTypeIsNotDevice(EntityType entityType) {
// GIVEN
var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(nonDeviceId)
.scope(AttributeScope.SERVER_SCOPE)
.keys(List.of("inactivityTimeout", "someOtherDeletedAttribute"))
.build();
given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys()));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@ParameterizedTest
@EnumSource(
value = AttributeScope.class,
names = {"SERVER_SCOPE"},
mode = EnumSource.Mode.EXCLUDE
)
void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButAttributeScopeIsNotServer(AttributeScope nonServerScope) {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(nonServerScope)
.keys(List.of("inactivityTimeout", "someOtherDeletedAttribute"))
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys()));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@Test
void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasNotDeleted() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.keys(List.of("someOtherDeletedAttribute"))
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys()));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
@Test
void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutDeleteFailed() {
// GIVEN
var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
var request = AttributesDeleteRequest.builder()
.tenantId(tenantId)
.entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE)
.keys(List.of("inactivityTimeout", "someOtherDeletedAttribute"))
.build();
given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFailedFuture(new RuntimeException("failed to delete")));
// WHEN
telemetryService.deleteAttributes(request);
// THEN
then(deviceStateManager).shouldHaveNoInteractions();
}
// used to emulate versions returned by save APIs
private static List<Long> listOfNNumbers(int N) { private static List<Long> listOfNNumbers(int N) {
return LongStream.range(0, N).boxed().toList(); return LongStream.range(0, N).boxed().toList();
} }

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

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -58,6 +59,7 @@ public class TenantProfile extends BaseData<TenantProfileId> implements HasName
@Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false") "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false")
private boolean isolatedTbRuleEngine; private boolean isolatedTbRuleEngine;
@Valid
@Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.")
private transient TenantProfileData profileData; private transient TenantProfileData profileData;
@JsonIgnore @JsonIgnore

7
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java

@ -15,12 +15,15 @@
*/ */
package org.thingsboard.server.common.data.edqs.fields; package org.thingsboard.server.common.data.edqs.fields;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder @SuperBuilder
@ -29,9 +32,9 @@ public class EntityViewFields extends AbstractEntityFields {
private String type; private String type;
private String additionalInfo; private String additionalInfo;
public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, String additionalInfo, Long version) { public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, JsonNode additionalInfo, Long version) {
super(id, createdTime, tenantId, customerId, name, version); super(id, createdTime, tenantId, customerId, name, version);
this.type = type; this.type = type;
this.additionalInfo = additionalInfo; this.additionalInfo = getText(additionalInfo);
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java

@ -289,7 +289,7 @@ public class FieldsUtil {
} }
public static String getText(JsonNode node) { public static String getText(JsonNode node) {
return node != null ? node.asText() : ""; return node != null ? node.toString() : "";
} }
private static UUID getCustomerId(CustomerId customerId) { private static UUID getCustomerId(CustomerId customerId) {

8
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.tenant.profile; package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -135,10 +136,17 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private double warnThreshold; private double warnThreshold;
@Schema(example = "5")
private long maxCalculatedFieldsPerEntity = 5; private long maxCalculatedFieldsPerEntity = 5;
@Schema(example = "10")
private long maxArgumentsPerCF = 10; private long maxArgumentsPerCF = 10;
@Builder.Default
@Min(value = 1, message = "must be at least 1")
@Schema(example = "1000")
private long maxDataPointsPerRollingArg = 1000; private long maxDataPointsPerRollingArg = 1000;
@Schema(example = "32")
private long maxStateSizeInKBytes = 32; private long maxStateSizeInKBytes = 32;
@Schema(example = "2")
private long maxSingleValueArgumentSizeInKBytes = 2; private long maxSingleValueArgumentSizeInKBytes = 2;
@Override @Override

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.tenant.profile; package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@ -27,6 +28,7 @@ public class TenantProfileData implements Serializable {
private static final long serialVersionUID = -3642550257035920976L; private static final long serialVersionUID = -3642550257035920976L;
@Valid
@Schema(description = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") @Schema(description = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.")
private TenantProfileConfiguration configuration; private TenantProfileConfiguration configuration;

3
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java

@ -430,6 +430,9 @@ public class TenantRepo {
private String getEntityName(EntityId entityId) { private String getEntityName(EntityId entityId) {
EntityType entityType = entityId.getEntityType(); EntityType entityType = entityId.getEntityType();
if (entityType == EntityType.TENANT && entityId.getId().equals(TenantId.NULL_UUID)) {
return "";
}
return switch (entityType) { return switch (entityType) {
case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName();
default -> throw new RuntimeException("Unsupported entity type: " + entityType); default -> throw new RuntimeException("Unsupported entity type: " + entityType);

13
common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java

@ -20,7 +20,6 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.edqs.EdqsEventType;
@ -43,18 +42,6 @@ public class EdqsStatsService {
private final ConcurrentHashMap<TenantId, EdqsStats> statsMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<TenantId, EdqsStats> statsMap = new ConcurrentHashMap<>();
private final StatsFactory statsFactory; private final StatsFactory statsFactory;
@Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}",
fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}")
private void reportStats() {
if (statsMap.isEmpty()) {
return;
}
String values = statsMap.entrySet().stream()
.map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]")
.collect(Collectors.joining(System.lineSeparator()));
log.info("EDQS Stats: {}", values);
}
public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) {
statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory))
.reportEvent(objectType, eventType); .reportEvent(objectType, eventType);

2
common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java

@ -26,8 +26,6 @@ import java.util.Set;
@Data @Data
public class CalculatedFieldPartitionChangeMsg implements ToCalculatedFieldSystemMsg { public class CalculatedFieldPartitionChangeMsg implements ToCalculatedFieldSystemMsg {
private final boolean[] partitions;
@Override @Override
public TenantId getTenantId() { public TenantId getTenantId() {
return TenantId.SYS_TENANT_ID; return TenantId.SYS_TENANT_ID;

6
common/proto/src/main/proto/queue.proto

@ -1122,7 +1122,11 @@ message TbAttributeDeleteProto {
int64 tenantIdLSB = 5; int64 tenantIdLSB = 5;
string scope = 6; string scope = 6;
repeated string keys = 7; repeated string keys = 7;
bool notifyDevice = 8; // DEPRECATED. FOR REMOVAL
// Since 4.0, this field is no longer used.
// Device notifications are now handled directly by DefaultTelemetrySubscriptionService,
// eliminating the need to pass this parameter through the queue and proto to DefaultSubscriptionManagerService.
optional bool notifyDevice = 8 [deprecated = true];
} }
message TbTimeSeriesDeleteProto { message TbTimeSeriesDeleteProto {

41
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -67,8 +68,6 @@ public class HashPartitionService implements PartitionService {
private String cfEventTopic; private String cfEventTopic;
@Value("${queue.calculated_fields.state_topic:tb_cf_state}") @Value("${queue.calculated_fields.state_topic:tb_cf_state}")
private String cfStateTopic; private String cfStateTopic;
@Value("${queue.calculated_fields.partitions:10}")
private Integer cfPartitions;
@Value("${queue.vc.topic:tb_version_control}") @Value("${queue.vc.topic:tb_version_control}")
private String vcTopic; private String vcTopic;
@Value("${queue.vc.partitions:10}") @Value("${queue.vc.partitions:10}")
@ -122,11 +121,6 @@ public class HashPartitionService implements PartitionService {
partitionSizesMap.put(coreKey, corePartitions); partitionSizesMap.put(coreKey, corePartitions);
partitionTopicsMap.put(coreKey, coreTopic); partitionTopicsMap.put(coreKey, coreTopic);
partitionSizesMap.put(QueueKey.CF, cfPartitions);
partitionTopicsMap.put(QueueKey.CF, cfEventTopic);
partitionSizesMap.put(QueueKey.CF_STATES, cfPartitions);
partitionTopicsMap.put(QueueKey.CF_STATES, cfStateTopic);
QueueKey vcKey = new QueueKey(ServiceType.TB_VC_EXECUTOR); QueueKey vcKey = new QueueKey(ServiceType.TB_VC_EXECUTOR);
partitionSizesMap.put(vcKey, vcPartitions); partitionSizesMap.put(vcKey, vcPartitions);
partitionTopicsMap.put(vcKey, vcTopic); partitionTopicsMap.put(vcKey, vcTopic);
@ -165,6 +159,14 @@ public class HashPartitionService implements PartitionService {
List<QueueRoutingInfo> queueRoutingInfoList = getQueueRoutingInfos(); List<QueueRoutingInfo> queueRoutingInfoList = getQueueRoutingInfos();
queueRoutingInfoList.forEach(queue -> { queueRoutingInfoList.forEach(queue -> {
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue);
if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) {
QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME);
partitionSizesMap.put(cfQueueKey, queue.getPartitions());
partitionTopicsMap.put(cfQueueKey, cfEventTopic);
QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME);
partitionSizesMap.put(cfQueueStatesKey, queue.getPartitions());
partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic);
}
partitionTopicsMap.put(queueKey, queue.getQueueTopic()); partitionTopicsMap.put(queueKey, queue.getQueueTopic());
partitionSizesMap.put(queueKey, queue.getPartitions()); partitionSizesMap.put(queueKey, queue.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queue)); queueConfigs.put(queueKey, new QueueConfig(queue));
@ -213,6 +215,14 @@ public class HashPartitionService implements PartitionService {
QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg);
TenantId tenantId = queueRoutingInfo.getTenantId(); TenantId tenantId = queueRoutingInfo.getTenantId();
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId);
if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) {
QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME);
partitionSizesMap.put(cfQueueKey, queueRoutingInfo.getPartitions());
partitionTopicsMap.put(cfQueueKey, cfEventTopic);
QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME);
partitionSizesMap.put(cfQueueStatesKey, queueRoutingInfo.getPartitions());
partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic);
}
partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic());
partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo));
@ -252,6 +262,15 @@ public class HashPartitionService implements PartitionService {
partitionTopicsMap.remove(queueKey); partitionTopicsMap.remove(queueKey);
partitionSizesMap.remove(queueKey); partitionSizesMap.remove(queueKey);
queueConfigs.remove(queueKey); queueConfigs.remove(queueKey);
if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) {
QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME);
partitionSizesMap.remove(cfQueueKey);
partitionTopicsMap.remove(cfQueueKey);
QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME);
partitionSizesMap.remove(cfQueueStatesKey);
partitionTopicsMap.remove(cfQueueStatesKey);
}
} }
@Override @Override
@ -336,8 +355,7 @@ public class HashPartitionService implements PartitionService {
} }
} }
@Override private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
public TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
Integer partitionSize = partitionSizesMap.get(queueKey); Integer partitionSize = partitionSizesMap.get(queueKey);
if (partitionSize == null) { if (partitionSize == null) {
throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing");
@ -552,11 +570,6 @@ public class HashPartitionService implements PartitionService {
return list == null ? 0 : list.size(); return list == null ? 0 : list.size();
} }
@Override
public int getTotalCalculatedFieldPartitions() {
return cfPartitions;
}
private Map<QueueKey, List<ServiceInfo>> getServiceKeyListMap(List<ServiceInfo> services) { private Map<QueueKey, List<ServiceInfo>> getServiceKeyListMap(List<ServiceInfo> services) {
final Map<QueueKey, List<ServiceInfo>> currentMap = new HashMap<>(); final Map<QueueKey, List<ServiceInfo>> currentMap = new HashMap<>();
services.forEach(serviceInfo -> { services.forEach(serviceInfo -> {

4
common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java

@ -37,8 +37,6 @@ public interface PartitionService {
TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId);
TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId);
List<TopicPartitionInfo> resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); List<TopicPartitionInfo> resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId);
boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId);
@ -81,6 +79,4 @@ public interface PartitionService {
int resolvePartitionIndex(UUID entityId, int partitions); int resolvePartitionIndex(UUID entityId, int partitions);
int getTotalCalculatedFieldPartitions();
} }

3
common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java

@ -35,9 +35,6 @@ public class QueueKey {
private final String queueName; private final String queueName;
private final TenantId tenantId; private final TenantId tenantId;
public static final QueueKey CF = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_QUEUE_NAME);
public static final QueueKey CF_STATES = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_STATES_QUEUE_NAME);
public QueueKey(ServiceType type, Queue queue) { public QueueKey(ServiceType type, Queue queue) {
this.type = type; this.type = type;
this.queueName = queue.getName(); this.queueName = queue.getName();

4
common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java

@ -64,10 +64,10 @@ public class PartitionChangeEvent extends TbApplicationEvent {
} }
public Set<TopicPartitionInfo> getCfPartitions() { public Set<TopicPartitionInfo> getCfPartitions() {
return newPartitions.getOrDefault(QueueKey.CF, Collections.emptySet()); return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME);
} }
private Set<TopicPartitionInfo> getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { public Set<TopicPartitionInfo> getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) {
return newPartitions.entrySet() return newPartitions.entrySet()
.stream() .stream()
.filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName()))

2
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java

@ -494,7 +494,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder(); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings); consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic());
consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer")); consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));

2
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java

@ -440,7 +440,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { public TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder(); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings); consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic());
consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer")); consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));

6
common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java

@ -42,7 +42,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class ZkDiscoveryServiceTest { public class ZkDiscoveryServiceTest {

54
common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java

@ -20,12 +20,16 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfObject; import org.thingsboard.script.api.tbel.TbelCfObject;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -34,22 +38,31 @@ import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format; import static java.lang.String.format;
@Slf4j @Slf4j
public abstract class AbstractScriptInvokeService implements ScriptInvokeService { public abstract class AbstractScriptInvokeService implements ScriptInvokeService {
private static final String REQUESTS = "requests";
private static final String INVOKE_RESPONSES = "invoke_responses";
private static final String EVAL_RESPONSES = "eval_responses";
private static final String FAILURES = "failures";
private static final String TIMEOUTS = "timeouts";
protected final Map<UUID, BlockedScriptInfo> disabledScripts = new ConcurrentHashMap<>(); protected final Map<UUID, BlockedScriptInfo> disabledScripts = new ConcurrentHashMap<>();
private final AtomicInteger pushedMsgs = new AtomicInteger(0);
private final AtomicInteger invokeMsgs = new AtomicInteger(0);
private final AtomicInteger evalMsgs = new AtomicInteger(0);
protected final AtomicInteger failedMsgs = new AtomicInteger(0);
protected final AtomicInteger timeoutMsgs = new AtomicInteger(0);
private final FutureCallback<UUID> evalCallback = new ScriptStatCallback<>(evalMsgs, timeoutMsgs, failedMsgs); private StatsCounter requestsCounter;
private final FutureCallback<Object> invokeCallback = new ScriptStatCallback<>(invokeMsgs, timeoutMsgs, failedMsgs); private StatsCounter invokeResponsesCounter;
private StatsCounter evalResponsesCounter;
private StatsCounter failuresCounter;
private StatsCounter timeoutsCounter;
private FutureCallback<UUID> evalCallback;
private FutureCallback<Object> invokeCallback;
@Autowired
private StatsFactory statsFactory;
protected ScheduledExecutorService timeoutExecutorService; protected ScheduledExecutorService timeoutExecutorService;
@ -78,6 +91,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
protected abstract boolean isScriptPresent(UUID scriptId); protected abstract boolean isScriptPresent(UUID scriptId);
protected abstract boolean isExecEnabled(TenantId tenantId); protected abstract boolean isExecEnabled(TenantId tenantId);
protected abstract void reportExecution(TenantId tenantId, CustomerId customerId); protected abstract void reportExecution(TenantId tenantId, CustomerId customerId);
protected abstract ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames); protected abstract ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames);
@ -87,6 +101,14 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
protected abstract void doRelease(UUID scriptId) throws Exception; protected abstract void doRelease(UUID scriptId) throws Exception;
public void init() { public void init() {
String key = getStatsType().getName();
this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS);
this.invokeResponsesCounter = statsFactory.createStatsCounter(key, INVOKE_RESPONSES);
this.evalResponsesCounter = statsFactory.createStatsCounter(key, EVAL_RESPONSES);
this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES);
this.timeoutsCounter = statsFactory.createStatsCounter(key, TIMEOUTS);
this.evalCallback = new ScriptStatCallback<>(evalResponsesCounter, timeoutsCounter, failuresCounter);
this.invokeCallback = new ScriptStatCallback<>(invokeResponsesCounter, timeoutsCounter, failuresCounter);
if (getMaxEvalRequestsTimeout() > 0 || getMaxInvokeRequestsTimeout() > 0) { if (getMaxEvalRequestsTimeout() > 0 || getMaxInvokeRequestsTimeout() > 0) {
timeoutExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("script-timeout"); timeoutExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("script-timeout");
} }
@ -100,11 +122,11 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
public void printStats() { public void printStats() {
if (isStatsEnabled()) { if (isStatsEnabled()) {
int pushed = pushedMsgs.getAndSet(0); int pushed = requestsCounter.getAndClear();
int invoked = invokeMsgs.getAndSet(0); int invoked = invokeResponsesCounter.getAndClear();
int evaluated = evalMsgs.getAndSet(0); int evaluated = evalResponsesCounter.getAndClear();
int failed = failedMsgs.getAndSet(0); int failed = failuresCounter.getAndClear();
int timedOut = timeoutMsgs.getAndSet(0); int timedOut = timeoutsCounter.getAndClear();
if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) { if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) {
log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]", log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
getStatsName(), pushed, invoked + evaluated, invoked, evaluated, failed, timedOut); getStatsName(), pushed, invoked + evaluated, invoked, evaluated, failed, timedOut);
@ -119,7 +141,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
} }
UUID scriptId = UUID.randomUUID(); UUID scriptId = UUID.randomUUID();
pushedMsgs.incrementAndGet(); requestsCounter.increment();
return withTimeoutAndStatsCallback(scriptId, null, return withTimeoutAndStatsCallback(scriptId, null,
doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
} else { } else {
@ -141,7 +163,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t)); return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t));
} }
reportExecution(tenantId, customerId); reportExecution(tenantId, customerId);
pushedMsgs.incrementAndGet(); requestsCounter.increment();
log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout());
var task = doInvokeFunction(scriptId, args); var task = doInvokeFunction(scriptId, args);
@ -278,4 +300,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
private <T> ListenableFuture<T> error(String message) { private <T> ListenableFuture<T> error(String message) {
return Futures.immediateFailedFuture(new RuntimeException(message)); return Futures.immediateFailedFuture(new RuntimeException(message));
} }
protected abstract StatsType getStatsType();
} }

14
common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java

@ -19,29 +19,29 @@ import com.google.common.util.concurrent.FutureCallback;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.stats.StatsCounter;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
public class ScriptStatCallback<T> implements FutureCallback<T> { public class ScriptStatCallback<T> implements FutureCallback<T> {
private final AtomicInteger successMsgs; private final StatsCounter successMsgs;
private final AtomicInteger timeoutMsgs; private final StatsCounter timeoutMsgs;
private final AtomicInteger failedMsgs; private final StatsCounter failedMsgs;
@Override @Override
public void onSuccess(@Nullable T result) { public void onSuccess(@Nullable T result) {
successMsgs.incrementAndGet(); successMsgs.increment();
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
timeoutMsgs.incrementAndGet(); timeoutMsgs.increment();
} else { } else {
failedMsgs.incrementAndGet(); failedMsgs.increment();
} }
} }
} }

5
common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java

@ -26,6 +26,7 @@ import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
@ -117,4 +118,8 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic
.hash().toString(); .hash().toString();
} }
@Override
protected StatsType getStatsType() {
return StatsType.JS_INVOKE;
}
} }

7
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -44,6 +44,7 @@ import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
@ -139,6 +140,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
parserConfig.registerDataType("TbelCfCtx", TbelCfCtx.class, TbelCfCtx::memorySize);
TbUtils.register(parserConfig); TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
@ -262,4 +264,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
protected long getMaxEvalRequestsTimeout() { protected long getMaxEvalRequestsTimeout() {
return maxInvokeRequestsTimeout * 2; return maxInvokeRequestsTimeout * 2;
} }
@Override
protected StatsType getStatsType() {
return StatsType.TBEL_INVOKE;
}
} }

6
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -257,6 +257,8 @@ public class TbUtils {
float.class, int.class))); float.class, int.class)));
parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt",
double.class))); double.class)));
parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN",
double.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray", parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray",
@ -1161,6 +1163,10 @@ public class TbUtils {
return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue();
} }
public static boolean isNaN(double value) {
return Double.isNaN(value);
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) { public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) {
return toFlatMap(ctx, json, new ArrayList<>(), true); return toFlatMap(ctx, json, new ArrayList<>(), true);
} }

21
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java

@ -13,17 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.rule.engine.telemetry; package org.thingsboard.script.api.tbel;
import org.junit.jupiter.api.Test; import lombok.Getter;
import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections;
import java.util.Map;
class TbMsgAttributesNodeConfigurationTest { public class TbelCfCtx implements TbelCfObject {
@Test @Getter
void testDefaultConfig_givenUpdateAttributesOnlyOnValueChange_thenTrue_sinceVersion1() { private final Map<String, TbelCfArg> args;
assertThat(new TbMsgAttributesNodeConfiguration().defaultConfiguration().isUpdateAttributesOnlyOnValueChange()).isTrue();
public TbelCfCtx(Map<String, TbelCfArg> args) {
this.args = Collections.unmodifiableMap(args);
} }
@Override
public long memorySize() {
return OBJ_SIZE;
}
} }

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java

@ -17,6 +17,8 @@ package org.thingsboard.script.api.tbel;
public interface TbelCfObject { public interface TbelCfObject {
long OBJ_SIZE = 32L; // Approximate calculation;
long memorySize(); long memorySize();
} }

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java

@ -22,8 +22,6 @@ import lombok.Data;
@Data @Data
public class TbelCfSingleValueArg implements TbelCfArg { public class TbelCfSingleValueArg implements TbelCfArg {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts; private final long ts;
private final Object value; private final Object value;

8
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -1138,12 +1138,18 @@ public class TbUtilsTest {
} }
@Test @Test
void toInt() { public void toInt() {
Assertions.assertEquals(1729, TbUtils.toInt(doubleVal)); Assertions.assertEquals(1729, TbUtils.toInt(doubleVal));
Assertions.assertEquals(13, TbUtils.toInt(12.8)); Assertions.assertEquals(13, TbUtils.toInt(12.8));
Assertions.assertEquals(28, TbUtils.toInt(28.0)); Assertions.assertEquals(28, TbUtils.toInt(28.0));
} }
@Test
public void isNaN() {
Assertions.assertFalse(TbUtils.isNaN(doubleVal));
Assertions.assertTrue(TbUtils.isNaN(Double.NaN));
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

4
common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java

@ -41,6 +41,10 @@ public class DefaultCounter {
return aiCounter.get(); return aiCounter.get();
} }
public int getAndClear() {
return aiCounter.getAndSet(0);
}
public void add(int delta){ public void add(int delta){
aiCounter.addAndGet(delta); aiCounter.addAndGet(delta);
micrometerCounter.increment(delta); micrometerCounter.increment(delta);

1
common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java

@ -20,6 +20,7 @@ public enum StatsType {
CORE("core"), CORE("core"),
TRANSPORT("transport"), TRANSPORT("transport"),
JS_INVOKE("jsInvoke"), JS_INVOKE("jsInvoke"),
TBEL_INVOKE("tbelInvoke"),
RATE_EXECUTOR("rateExecutor"), RATE_EXECUTOR("rateExecutor"),
HOUSEKEEPER("housekeeper"), HOUSEKEEPER("housekeeper"),
EDGE("edge"), EDGE("edge"),

3
dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java

@ -29,6 +29,7 @@ import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.dao.model.sql.AuditLogEntity; import org.thingsboard.server.dao.model.sql.AuditLogEntity;
import org.thingsboard.server.dao.model.sql.CalculatedFieldDebugEventEntity;
import org.thingsboard.server.dao.model.sql.ErrorEventEntity; import org.thingsboard.server.dao.model.sql.ErrorEventEntity;
import org.thingsboard.server.dao.model.sql.LifecycleEventEntity; import org.thingsboard.server.dao.model.sql.LifecycleEventEntity;
import org.thingsboard.server.dao.model.sql.RuleChainDebugEventEntity; import org.thingsboard.server.dao.model.sql.RuleChainDebugEventEntity;
@ -68,7 +69,7 @@ public class DedicatedEventsJpaDaoConfig {
EntityManagerFactoryBuilder builder) { EntityManagerFactoryBuilder builder) {
return builder return builder
.dataSource(eventsDataSource) .dataSource(eventsDataSource)
.packages(LifecycleEventEntity.class, StatisticsEventEntity.class, ErrorEventEntity.class, RuleNodeDebugEventEntity.class, RuleChainDebugEventEntity.class, AuditLogEntity.class) .packages(LifecycleEventEntity.class, StatisticsEventEntity.class, ErrorEventEntity.class, RuleNodeDebugEventEntity.class, RuleChainDebugEventEntity.class, AuditLogEntity.class, CalculatedFieldDebugEventEntity.class)
.persistenceUnit(EVENTS_PERSISTENCE_UNIT) .persistenceUnit(EVENTS_PERSISTENCE_UNIT)
.build(); .build();
} }

1
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -379,6 +379,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
if (defaultConfig != null) { if (defaultConfig != null) {
updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks); updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks);
updated |= convertToImageUrls(tenantId, prefix, defaultConfig, imagesLinks);
widgetTypeDetails.setDefaultConfig(defaultConfig); widgetTypeDetails.setDefaultConfig(defaultConfig);
} }
} }

5
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -147,7 +147,8 @@ public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UU
@Query("SELECT externalId FROM EntityViewEntity WHERE id = :id") @Query("SELECT externalId FROM EntityViewEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id); UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT new org.thingsboard.server.common.data.edqs.fields.GenericFields(e.id, e.createdTime, e.tenantId," + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.EntityViewFields(e.id, e.createdTime, e.tenantId, " +
"e.name, e.version) FROM EntityViewEntity e WHERE e.id > :id ORDER BY e.id") "e.customerId, e.name, e.type, e.additionalInfo, e.version) " +
"FROM EntityViewEntity e WHERE e.id > :id ORDER BY e.id")
List<EntityViewFields> findNextBatch(@Param("id") UUID id, Limit limit); List<EntityViewFields> findNextBatch(@Param("id") UUID id, Limit limit);
} }

2
dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql

@ -36,3 +36,5 @@ CREATE INDEX IF NOT EXISTS idx_lc_event_main
CREATE INDEX IF NOT EXISTS idx_error_event_main CREATE INDEX IF NOT EXISTS idx_error_event_main
ON error_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); ON error_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95);
CREATE INDEX IF NOT EXISTS idx_cf_debug_event_main
ON cf_debug_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95);

4
docker/docker-compose.edqs.volumes.yml

@ -19,10 +19,10 @@ version: '3.0'
services: services:
tb-edqs-1: tb-edqs-1:
volumes: volumes:
- tb-edqs-log-volume:/var/log/edqs - tb-edqs-log-volume:/var/log/tb-edqs
tb-edqs-2: tb-edqs-2:
volumes: volumes:
- tb-edqs-log-volume:/var/log/edqs - tb-edqs-log-volume:/var/log/tb-edqs
volumes: volumes:
tb-edqs-log-volume: tb-edqs-log-volume:

12
docker/docker-compose.edqs.yml

@ -33,10 +33,10 @@ services:
restart: always restart: always
image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}"
env_file: env_file:
- edqs.env - tb-edqs.env
volumes: volumes:
- ./edqs/conf:/usr/share/edqs/conf - ./tb-edqs/conf:/usr/share/tb-edqs/conf
- ./edqs/log:/var/log/edqs - ./tb-edqs/log:/var/log/tb-edqs
ports: ports:
- "8080" - "8080"
depends_on: depends_on:
@ -46,10 +46,10 @@ services:
restart: always restart: always
image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}"
env_file: env_file:
- edqs.env - tb-edqs.env
volumes: volumes:
- ./edqs/conf:/usr/share/edqs/conf - ./tb-edqs/conf:/usr/share/tb-edqs/conf
- ./edqs/log:/var/log/edqs - ./tb-edqs/log:/var/log/tb-edqs
ports: ports:
- "8080" - "8080"
depends_on: depends_on:

98
docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json

@ -223,8 +223,8 @@
"fill": 1, "fill": 1,
"fillGradient": 0, "fillGradient": 0,
"gridPos": { "gridPos": {
"h": 12, "h": 10,
"w": 24, "w": 12,
"x": 0, "x": 0,
"y": 10 "y": 10
}, },
@ -303,6 +303,100 @@
"align": false, "align": false,
"alignLevel": null "alignLevel": null
} }
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 10,
"w": 12,
"x": 12,
"y": 10
},
"hiddenSeries": false,
"id": 19,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.4",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"exemplar": true,
"expr": "sum by(statsName) (increase(tbelInvoke_total[1m]))",
"interval": "",
"legendFormat": "{{statsName}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "TbelInvoke Stats",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
} }
], ],
"schemaVersion": 27, "schemaVersion": 27,

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

Loading…
Cancel
Save