Browse Source

Merge branch 'master' into improvement/tbel-utils-autcompletes

pull/12856/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
29f0541966
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 234
      application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg
  2. 466
      application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg
  3. 582
      application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg
  4. 200
      application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg
  5. 232
      application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg
  6. 466
      application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg
  7. 234
      application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg
  8. 203
      application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg
  9. 200
      application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg
  10. 466
      application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg
  11. 234
      application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg
  12. 466
      application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg
  13. 200
      application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg
  14. 8
      application/src/main/data/json/system/widget_bundles/maps.json
  15. 4
      application/src/main/data/json/system/widget_types/google_map.json
  16. 4
      application/src/main/data/json/system/widget_types/here_map.json
  17. 95
      application/src/main/data/json/system/widget_types/image_map.json
  18. 102
      application/src/main/data/json/system/widget_types/image_map_deprecated.json
  19. 44
      application/src/main/data/json/system/widget_types/map.json
  20. 4
      application/src/main/data/json/system/widget_types/markers_placement___google_maps.json
  21. 4
      application/src/main/data/json/system/widget_types/markers_placement___image_map.json
  22. 4
      application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json
  23. 4
      application/src/main/data/json/system/widget_types/openstreet_map.json
  24. 80
      application/src/main/data/json/system/widget_types/route_map.json
  25. 2
      application/src/main/data/json/system/widget_types/route_map___google.json
  26. 2
      application/src/main/data/json/system/widget_types/route_map___openstreet.json
  27. 2
      application/src/main/data/json/system/widget_types/route_map___tencent.json
  28. 4
      application/src/main/data/json/system/widget_types/tencent_map.json
  29. 2
      application/src/main/data/json/system/widget_types/trip_animation.json
  30. 47
      application/src/main/data/json/system/widget_types/trip_map.json
  31. 16
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  32. 18
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  33. 10
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  34. 21
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  35. 32
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  36. 7
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java
  37. 12
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldInitService.java
  38. 4
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  39. 8
      application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java
  40. 11
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  41. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  42. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  43. 18
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  44. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  45. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  46. 83
      application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java
  47. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  48. 9
      application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java
  49. 11
      application/src/main/resources/thingsboard.yml
  50. 21
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  51. 55
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  52. 79
      application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java
  53. 1
      application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java
  54. 4
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  55. 5
      application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java
  56. 8
      application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java
  57. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java
  58. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java
  59. 13
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java
  60. 13
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java
  61. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java
  62. 41
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  63. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java
  64. 3
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java
  65. 4
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java
  66. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  67. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  68. 54
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  69. 14
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java
  70. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java
  71. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  72. 8
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  73. 8
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  74. 4
      common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java
  75. 1
      common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java
  76. 1
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  77. 4
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  78. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  79. 73
      docker/compose-utils.sh
  80. 11
      docker/docker-check-log-folders.sh
  81. 4
      docker/docker-compose.edqs.volumes.yml
  82. 12
      docker/docker-compose.edqs.yml
  83. 2
      docker/docker-compose.yml
  84. 2
      docker/docker-create-log-folders.sh
  85. 2
      docker/docker-install-tb.sh
  86. 2
      docker/docker-start-services.sh
  87. 2
      docker/docker-upgrade-tb.sh
  88. 98
      docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json
  89. 0
      docker/tb-edqs.env
  90. 4
      docker/tb-edqs/conf/logback.xml
  91. 4
      docker/tb-edqs/conf/tb-edqs.conf
  92. 2
      edqs/src/main/resources/edqs.yml
  93. 1
      msa/tb/docker-cassandra/Dockerfile
  94. 1
      msa/tb/docker-postgres/Dockerfile
  95. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  96. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  97. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  98. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  99. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  100. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

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

@ -3,6 +3,7 @@
"description": "Bottom right elbow connector",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "line",
@ -10,23 +11,132 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"tags": [
{
"tag": "line",
@ -15,23 +16,364 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"tags": [
{
"tag": "line",
@ -15,23 +16,480 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -42,12 +500,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -56,32 +513,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "arrow",
@ -85,6 +86,83 @@
},
"defaultSetValueSettings": 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": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}",
"id": "flowAnimationWidth",
"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",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "line",
@ -10,23 +11,132 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -51,32 +160,93 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"tags": [
{
"tag": "line",
@ -15,23 +16,364 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "line",
@ -10,23 +11,132 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

203
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.",
"widgetSizeX": 2,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "arrow",
@ -86,6 +87,83 @@
},
"defaultSetValueSettings": 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": [
@ -94,16 +172,8 @@
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -114,12 +184,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -128,49 +197,93 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}",
"id": "flowAnimationWidth",
"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",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"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>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/>
<path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/>
</svg>
<path d="m0 100h400" stroke="#1A1A1A" stroke-width="6" tb:tag="line"/><path d="m229 100-58 29v-58z" fill="#1a1a1a" tb:tag="arrow"/><g tb:tag="animationGroup"/>
</svg>

Before

Width:  |  Height:  |  Size: 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.",
"widgetSizeX": 1,
"widgetSizeY": 2,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "arrow",
@ -85,6 +86,83 @@
},
"defaultSetValueSettings": 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": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}",
"id": "flowAnimationWidth",
"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",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"tags": [
{
"tag": "line",
@ -15,23 +16,364 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "line",
@ -10,23 +11,132 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -37,12 +147,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -51,32 +160,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}",
"tags": [
{
"tag": "line",
@ -15,23 +16,364 @@
"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": [
{
"id": "mainLine",
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -42,12 +384,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -56,32 +397,95 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "flowAnimationWidth",
"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",
"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>
<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>

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.",
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n",
"tags": [
{
"tag": "arrow",
@ -85,6 +86,83 @@
},
"defaultSetValueSettings": 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": [
@ -93,16 +171,8 @@
"name": "{i18n:scada.symbol.main-line}",
"type": "switch",
"default": true,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "mainLineSize",
@ -113,12 +183,11 @@
"subLabel": "Main",
"divider": true,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "secondaryLineSize",
@ -127,48 +196,93 @@
"default": 2,
"required": true,
"subLabel": "Secondary",
"divider": null,
"fieldSuffix": "px",
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0,
"max": 99,
"step": 1
"step": 1,
"disabled": false,
"visible": true
},
{
"id": "lineColor",
"name": "{i18n:scada.symbol.line-color}",
"type": "color",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"disabled": false,
"visible": true
},
{
"id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}",
"id": "flowAnimationWidth",
"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",
"default": "#1A1A1A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
"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>
<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>

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",
"name": "Markers Placement - Google Maps",
"deprecated": false,
"deprecated": true,
"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.",
"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}",
"settingsSchema": "",
"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}"
},
"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",
"name": "Markers Placement - Image Map",
"deprecated": false,
"deprecated": true,
"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.",
"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}",
"settingsSchema": "",
"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}"
},
"tags": [

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

@ -1,7 +1,7 @@
{
"fqn": "input_widgets.markers_placement_openstreetmap",
"name": "Markers Placement - OpenStreetMap",
"deprecated": false,
"deprecated": true,
"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.",
"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}",
"settingsSchema": "",
"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}"
},
"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",
"name": "Route Map - Google",
"deprecated": false,
"deprecated": true,
"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.",
"descriptor": {

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

@ -1,7 +1,7 @@
{
"fqn": "maps_v2.route_map_openstreetmap",
"name": "Route Map - OpenStreet",
"deprecated": false,
"deprecated": true,
"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.",
"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",
"name": "Route Map - Tencent",
"deprecated": false,
"deprecated": true,
"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.",
"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",
"name": "Trip Animation",
"deprecated": false,
"deprecated": true,
"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.",
"descriptor": {

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

File diff suppressed because one or more lines are too long

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.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
@ -432,10 +431,6 @@ public class ActorSystemContext {
@Getter
private ClaimDevicesService claimDevicesService;
@Autowired
@Getter
private JsInvokeStats jsInvokeStats;
//TODO: separate context for TbCore and TbRuleEngine
@Autowired(required = false)
@Getter
@ -595,17 +590,6 @@ public class ActorSystemContext {
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}")
@Getter
private boolean tenantComponentsInitEnabled;

18
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.shared.AbstractContextAwareMsgProcessor;
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.cf.configuration.Argument;
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.msg.TbMsgType;
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.gen.transport.TransportProtos.AttributeScopeProto;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
@ -74,7 +76,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
final EntityId entityId;
final CalculatedFieldProcessingService cfService;
final CalculatedFieldStateService cfStateService;
final int partition;
TbActorCtx ctx;
Map<CalculatedFieldId, CalculatedFieldState> states = new HashMap<>();
@ -85,7 +86,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
this.entityId = entityId;
this.cfService = systemContext.getCalculatedFieldProcessingService();
this.cfStateService = systemContext.getCalculatedFieldStateService();
this.partition = systemContext.getCalculatedFieldEntityProfileCache().getEntityIdPartition(tenantId, entityId);
}
void init(TbActorCtx ctx) {
@ -93,8 +93,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
public void process(CalculatedFieldPartitionChangeMsg msg) {
if (!msg.getPartitions()[partition]) {
log.info("[{}][{}] Stopping entity actor due to change partition event.", partition, entityId);
if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) {
log.info("[{}] Stopping entity actor due to change partition event.", entityId);
ctx.stop(ctx.getSelf());
}
}
@ -153,7 +153,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
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 numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size());
MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback());
@ -168,7 +168,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
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 ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback());
@ -287,7 +287,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeChecked = true;
if (state.isSizeOk()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (!calculationResult.isEmpty()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
} else {
callback.onSuccess();
}
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
}

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

@ -318,14 +318,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
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
MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback());
// process all cfs related to entity, or it's profile;
var entityIdFields = getCalculatedFieldsByEntityId(entityId);
var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId));
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));
} else {
callback.onSuccess();
@ -342,7 +342,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) {
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 linksList = proto.getLinksList();
for (var linkProto : linksList) {
@ -357,14 +357,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback());
var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback);
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);
});
} else {
msg.getCallback().onSuccess();
}
} 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());
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
public String 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);
if (isRuleEngine) {
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 {
if (getApiUsageState().isReExecEnabled()) {
log.debug("[{}] Going to init rule chains", tenantId);
@ -98,11 +107,6 @@ public class TenantActor extends RuleChainManagerActor {
} else {
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) {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
cantFindTenant = true;
@ -185,6 +189,10 @@ public class TenantActor extends RuleChainManagerActor {
}
private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) {
if (cfActor == null) {
log.warn("[{}] CF Actor is not initialized.", tenantId);
return;
}
if (priority) {
cfActor.tellWithHighPriority(msg);
} else {
@ -251,11 +259,25 @@ public class TenantActor extends RuleChainManagerActor {
ServiceType serviceType = msg.getServiceType();
if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) {
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) {
log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId);
initRuleChains();
}
} else {
if (cfActor != null) {
ctx.stop(cfActor.getActorId());
}
if (ruleChainsInitialized) {
log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId);
destroyRuleChains();

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

@ -27,4 +27,11 @@ public final class CalculatedFieldResult {
private final AttributeScope scope;
private final JsonNode result;
public boolean isEmpty() {
return result == null || result.isMissingNode() || result.isNull() ||
(result.isObject() && result.isEmpty()) ||
(result.isArray() && result.isEmpty()) ||
(result.isTextual() && result.asText().isEmpty());
}
}

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

@ -47,12 +47,20 @@ public class DefaultCalculatedFieldInitService implements CalculatedFieldInitSer
PageDataIterable<ProfileEntityIdInfo> deviceIdInfos = new PageDataIterable<>(deviceService::findProfileEntityIdInfos, initFetchPackSize);
for (ProfileEntityIdInfo idInfo : deviceIdInfos) {
log.trace("Processing device record: {}", idInfo);
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
try {
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process device record: {}", idInfo, e);
}
}
PageDataIterable<ProfileEntityIdInfo> assetIdInfos = new PageDataIterable<>(assetService::findProfileEntityIdInfos, initFetchPackSize);
for (ProfileEntityIdInfo idInfo : assetIdInfos) {
log.trace("Processing asset record: {}", idInfo);
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
try {
entityProfileCache.add(idInfo.getTenantId(), idInfo.getProfileId(), idInfo.getEntityId());
} catch (Exception e) {
log.error("Failed to process asset record: {}", idInfo, e);
}
}
}

4
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.MultipleTbCallback;
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.StringUtils;
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.msg.TbMsg;
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.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
@ -200,7 +202,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP
if (broadcast) {
broadcasts.add(link);
} 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);
}
}

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.extern.slf4j.Slf4j;
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.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
@ -57,7 +59,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override
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);
tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache())
.add(profileId, entityId, partition, tpi.isMyPartition());
@ -65,7 +67,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override
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 cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache());
//TODO: make this method atomic;
@ -86,7 +88,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent
@Override
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);
}

11
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.TsKvEntry;
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.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -44,7 +43,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
public class CalculatedFieldCtx {
@ -58,8 +56,6 @@ public class CalculatedFieldCtx {
private final Map<String, Argument> arguments;
private final Map<ReferencedEntityKey, String> mainEntityArguments;
private final Map<EntityId, Map<ReferencedEntityKey, String>> linkedEntityArguments;
private final Map<TbPair<EntityId, ReferencedEntityKey>, String> referencedEntityKeys;
private final List<String> argNames;
private Output output;
private String expression;
@ -87,17 +83,12 @@ public class CalculatedFieldCtx {
for (Map.Entry<String, Argument> entry : arguments.entrySet()) {
var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey();
if (refId == null) {
if (refId == null || refId.equals(calculatedField.getEntityId())) {
mainEntityArguments.put(refKey, entry.getKey());
} else {
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.output = configuration.getOutput();
this.expression = configuration.getExpression();

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.boot.autoconfigure.condition.ConditionalOnExpression;
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.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
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.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
@ -67,9 +69,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
@Override
public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) {
super.init(eventConsumer);
var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME);
this.stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<CalculatedFieldStateProto>>create()
.queueKey(QueueKey.CF_STATES)
.topic(partitionService.getTopic(QueueKey.CF_STATES))
.queueKey(queueKey)
.topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval)
.msgPackProcessor((msgs, consumer, config) -> {
for (TbProtoQueueMsg<CalculatedFieldStateProto> msg : msgs) {
@ -101,7 +105,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
@Override
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);
if (stateMsgProto == null) {
putStateId(msg.getHeaders(), stateId);

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

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("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 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.calculatedField.CalculatedFieldLinkedTelemetryMsg;
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.TenantId;
import org.thingsboard.server.common.data.queue.QueueConfig;
@ -79,8 +80,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
private long pollInterval;
@Value("${queue.calculated_fields.pack_processing_timeout:60000}")
private long packProcessingTimeout;
@Value("${queue.calculated_fields.pool_size:8}")
private int poolSize;
private final TbRuleEngineQueueFactory queueFactory;
private final CalculatedFieldStateService stateService;
@ -108,9 +107,10 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
public void init() {
super.init("tb-cf");
var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME);
this.eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create()
.queueKey(QueueKey.CF)
.topic(partitionService.getTopic(QueueKey.CF))
.queueKey(queueKey)
.topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval)
.msgPackProcessor(this::processMsgs)
.consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer())
@ -140,20 +140,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer
// 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.
actorContext.tell(new CalculatedFieldPartitionChangeMsg(partitionsToBooleanIndexArray(partitions)));
actorContext.tell(new CalculatedFieldPartitionChangeMsg());
} catch (Throwable 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 {
List<IdMsgPair<ToCalculatedFieldMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList();
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
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);
}
@ -371,7 +371,7 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
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);
toRuleEngineNfs.incrementAndGet();
}

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.stereotype.Service;
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.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
@ -108,7 +109,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
@Override
protected void onTbApplicationEvent(PartitionChangeEvent event) {
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;
}
if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) {

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

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

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

9
application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java

@ -53,8 +53,6 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
@ConditionalOnExpression("'${queue.type:null}'=='kafka' && ${edges.enabled:true} && ${sql.ttl.edge_events.edge_events_ttl:0} > 0")
public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
private static final String EDGE_EVENT_TOPIC_NAME = "tb_edge_event.notifications.";
private final TopicService topicService;
private final TenantService tenantService;
private final EdgeService edgeService;
@ -64,6 +62,9 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.edge_events.edge_events_ttl:2628000}")
private long ttlSeconds;
@Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}")
private String tbEdgeEventNotificationsTopic;
public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService,
TenantService tenantService, AttributesService attributesService,
TopicService topicService, TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs) {
@ -86,7 +87,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
return;
}
String edgeTopicPrefix = topicService.buildTopicName(EDGE_EVENT_TOPIC_NAME);
String edgeTopicPrefix = topicService.buildTopicName(tbEdgeEventNotificationsTopic);
List<String> matchingTopics = topics.stream().filter(topic -> topic.startsWith(edgeTopicPrefix)).toList();
if (matchingTopics.isEmpty()) {
log.debug("No matching topics found with prefix [{}]. Skipping cleanup.", edgeTopicPrefix);
@ -147,7 +148,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService {
try {
String remaining = topic.substring(prefix.length());
String[] parts = remaining.split("\\.");
TenantId tenantId = new TenantId(UUID.fromString(parts[0]));
TenantId tenantId = TenantId.fromUUID(UUID.fromString(parts[0]));
EdgeId edgeId = new EdgeId(UUID.fromString(parts[1]));
tenantEdgeMap.computeIfAbsent(tenantId, id -> new ArrayList<>()).add(edgeId);
} catch (Exception e) {

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

@ -502,8 +502,6 @@ actors:
statistics:
# Enable/disable actor statistics
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
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
calculated_fields:
@ -1284,7 +1282,7 @@ transport:
# URL of gateways dashboard repository
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: "${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: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
@ -1759,9 +1757,6 @@ queue:
stats:
# Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Statistics printing interval for EDQS
print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}"
vc:
# Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"
@ -1818,9 +1813,7 @@ queue:
# For high-priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}"
# Interval in milliseconds to poll messages by CF (Rule Engine) microservices
poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}"
# Amount of partitions used by CF microservices
partitions: "${TB_QUEUE_CF_PARTITIONS:10}"
poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:1000}"
# Timeout for processing a message pack by CF microservices
pack_processing_timeout: "${TB_QUEUE_CF_PACK_PROCESSING_TIMEOUT_MS:60000}"
# Thread pool size for processing of the incoming messages

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

@ -48,6 +48,9 @@ import static org.awaitility.Awaitility.await;
@DaoSqlTest
public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest {
public static final int TIMEOUT = 60;
public static final int POLL_INTERVAL = 1;
@BeforeEach
void setUp() throws Exception {
loginTenantAdmin();
@ -86,6 +89,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -95,6 +99,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -108,6 +113,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
@ -119,6 +125,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
@ -129,6 +136,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
@ -166,6 +174,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> state is not ready -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -175,6 +184,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -213,6 +223,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation with default value").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -222,6 +233,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -277,6 +289,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF and perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -292,6 +305,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":25}"));
await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -307,6 +321,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":15}"));
await().alias("update asset 1 telemetry -> recalculate state only for asset 1").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -322,6 +337,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":5}"));
await().alias("update asset 2 telemetry -> recalculate state only for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1 (no changes)
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -339,6 +355,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
Asset finalAsset3 = asset3;
await().alias("add new entity to profile -> calculate state for new entity").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 3
ArrayNode z3 = getServerAttributes(finalAsset3.getId(), "z");
@ -349,6 +366,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":20}"));
await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -375,6 +393,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
Asset updatedAsset3 = asset3;
await().alias("update device telemetry -> recalculate state for asset 1 and asset 2").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
// result of asset 1
ArrayNode z1 = getServerAttributes(asset1.getId(), "z");
@ -425,6 +444,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();
@ -434,6 +454,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp");
assertThat(fahrenheitTemp).isNotNull();

55
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -28,6 +28,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -73,6 +74,7 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
@ -325,6 +327,59 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
doDelete("/api/queues/" + queue.getUuidId()).andExpect(status().isOk());
}
@Test
public void testQueueWithReservedName() throws Exception {
loginSysAdmin();
// create queue
Queue queue = new Queue();
queue.setName(DataConstants.CF_QUEUE_NAME);
queue.setTopic("tb_rule_engine.calculated_fields");
queue.setPollInterval(25);
queue.setPartitions(10);
queue.setTenantId(TenantId.SYS_TENANT_ID);
queue.setConsumerPerPartition(false);
queue.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy = new SubmitStrategy();
submitStrategy.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR);
queue.setSubmitStrategy(submitStrategy);
ProcessingStrategy processingStrategy = new ProcessingStrategy();
processingStrategy.setType(ProcessingStrategyType.RETRY_ALL);
processingStrategy.setRetries(3);
processingStrategy.setFailurePercentage(0.7);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(5);
queue.setProcessingStrategy(processingStrategy);
doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", DataConstants.CF_QUEUE_NAME))));
// create queue
Queue queue2 = new Queue();
queue2.setName(DataConstants.CF_STATES_QUEUE_NAME);
queue2.setTopic("tb_rule_engine.calculated_fields");
queue2.setPollInterval(25);
queue2.setPartitions(10);
queue2.setTenantId(TenantId.SYS_TENANT_ID);
queue2.setConsumerPerPartition(false);
queue2.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy2 = new SubmitStrategy();
submitStrategy2.setType(SubmitStrategyType.SEQUENTIAL_BY_ORIGINATOR);
queue2.setSubmitStrategy(submitStrategy);
ProcessingStrategy processingStrategy2 = new ProcessingStrategy();
processingStrategy2.setType(ProcessingStrategyType.RETRY_ALL);
processingStrategy2.setRetries(3);
processingStrategy2.setFailurePercentage(0.7);
processingStrategy2.setPauseBetweenRetries(3);
processingStrategy2.setMaxPauseBetweenRetries(5);
queue2.setProcessingStrategy(processingStrategy);
doPost("/api/queues?serviceType=" + "TB-RULE-ENGINE", queue2)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", DataConstants.CF_STATES_QUEUE_NAME))));
}
private Queue saveQueue(Queue queue) {
return doPost("/api/queues?serviceType=TB_RULE_ENGINE", queue, Queue.class);
}

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

1
application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java

@ -428,7 +428,6 @@ public class HashPartitionServiceTest {
ReflectionTestUtils.setField(partitionService, "corePartitions", 10);
ReflectionTestUtils.setField(partitionService, "cfEventTopic", "tb_cf_event");
ReflectionTestUtils.setField(partitionService, "cfStateTopic", "tb_cf_state");
ReflectionTestUtils.setField(partitionService, "cfPartitions", 10);
ReflectionTestUtils.setField(partitionService, "vcTopic", "tb.vc");
ReflectionTestUtils.setField(partitionService, "vcPartitions", 10);
ReflectionTestUtils.setField(partitionService, "hashFunctionName", hashFunctionName);

4
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;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.kv.DoubleDataEntry;
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.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.Mockito.when;
@SpringBootTest(classes = DefaultTbelInvokeService.class)
@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class})
public class ScriptCalculatedFieldStateTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382"));

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
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.TbelInvokeService;
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.UUID;
@ -30,7 +31,7 @@ import java.util.concurrent.ExecutionException;
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 {
@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.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.ApiUsageState;
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.TbApiUsageStateClient;
import org.thingsboard.server.gen.js.JsInvokeProtos;
@ -68,6 +72,10 @@ class RemoteJsInvokeServiceTest {
remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient));
jsRequestTemplate = mock(TbQueueRequestTemplate.class);
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

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;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText;
@Data
@NoArgsConstructor
@SuperBuilder
@ -29,9 +32,9 @@ public class EntityViewFields extends AbstractEntityFields {
private String type;
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);
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) {
return node != null ? node.asText() : "";
return node != null ? node.toString() : "";
}
private static UUID getCustomerId(CustomerId customerId) {

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

@ -422,16 +422,19 @@ public class TenantRepo {
}
public String getOwnerName(EntityId ownerId) {
if (ownerId == null || (EntityType.CUSTOMER.equals(ownerId.getEntityType()) && CustomerId.NULL_UUID.equals(ownerId.getId()))) {
ownerId = tenantId;
if (ownerId == null || (ownerId.getEntityType() == EntityType.CUSTOMER && ownerId.isNullUid())) {
return getOwnerEntityName(tenantId);
}
return getEntityName(ownerId);
return getOwnerEntityName(ownerId);
}
private String getEntityName(EntityId entityId) {
private String getOwnerEntityName(EntityId entityId) {
EntityType entityType = entityId.getEntityType();
return switch (entityType) {
case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName();
case CUSTOMER, TENANT -> {
EntityFields fields = getEntityMap(entityType).get(entityId.getId()).getFields();
yield fields != null ? fields.getName() : "";
}
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.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType;
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 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) {
statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory))
.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
public class CalculatedFieldPartitionChangeMsg implements ToCalculatedFieldSystemMsg {
private final boolean[] partitions;
@Override
public TenantId getTenantId() {
return TenantId.SYS_TENANT_ID;

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.context.ApplicationEventPublisher;
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.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -67,8 +68,6 @@ public class HashPartitionService implements PartitionService {
private String cfEventTopic;
@Value("${queue.calculated_fields.state_topic:tb_cf_state}")
private String cfStateTopic;
@Value("${queue.calculated_fields.partitions:10}")
private Integer cfPartitions;
@Value("${queue.vc.topic:tb_version_control}")
private String vcTopic;
@Value("${queue.vc.partitions:10}")
@ -122,11 +121,6 @@ public class HashPartitionService implements PartitionService {
partitionSizesMap.put(coreKey, corePartitions);
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);
partitionSizesMap.put(vcKey, vcPartitions);
partitionTopicsMap.put(vcKey, vcTopic);
@ -165,6 +159,14 @@ public class HashPartitionService implements PartitionService {
List<QueueRoutingInfo> queueRoutingInfoList = getQueueRoutingInfos();
queueRoutingInfoList.forEach(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());
partitionSizesMap.put(queueKey, queue.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queue));
@ -213,6 +215,14 @@ public class HashPartitionService implements PartitionService {
QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg);
TenantId tenantId = queueRoutingInfo.getTenantId();
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());
partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions());
queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo));
@ -252,6 +262,15 @@ public class HashPartitionService implements PartitionService {
partitionTopicsMap.remove(queueKey);
partitionSizesMap.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
@ -336,8 +355,7 @@ public class HashPartitionService implements PartitionService {
}
}
@Override
public TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
Integer partitionSize = partitionSizesMap.get(queueKey);
if (partitionSize == null) {
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();
}
@Override
public int getTotalCalculatedFieldPartitions() {
return cfPartitions;
}
private Map<QueueKey, List<ServiceInfo>> getServiceKeyListMap(List<ServiceInfo> services) {
final Map<QueueKey, List<ServiceInfo>> currentMap = new HashMap<>();
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(QueueKey queueKey, EntityId entityId);
List<TopicPartitionInfo> resolveAll(ServiceType serviceType, String queueName, 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 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 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) {
this.type = type;
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() {
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()
.stream()
.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) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
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.groupId(topicService.buildTopicName("monolith-edge-event-consumer"));
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) {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
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.groupId(topicService.buildTopicName("tb-core-edge-event-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders()));

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.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfObject;
import org.thingsboard.server.common.data.id.CustomerId;
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.UUID;
@ -34,22 +38,31 @@ import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
@Slf4j
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<>();
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 final FutureCallback<Object> invokeCallback = new ScriptStatCallback<>(invokeMsgs, timeoutMsgs, failedMsgs);
private StatsCounter requestsCounter;
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;
@ -78,6 +91,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
protected abstract boolean isScriptPresent(UUID scriptId);
protected abstract boolean isExecEnabled(TenantId tenantId);
protected abstract void reportExecution(TenantId tenantId, CustomerId customerId);
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;
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) {
timeoutExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("script-timeout");
}
@ -100,11 +122,11 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
public void printStats() {
if (isStatsEnabled()) {
int pushed = pushedMsgs.getAndSet(0);
int invoked = invokeMsgs.getAndSet(0);
int evaluated = evalMsgs.getAndSet(0);
int failed = failedMsgs.getAndSet(0);
int timedOut = timeoutMsgs.getAndSet(0);
int pushed = requestsCounter.getAndClear();
int invoked = invokeResponsesCounter.getAndClear();
int evaluated = evalResponsesCounter.getAndClear();
int failed = failuresCounter.getAndClear();
int timedOut = timeoutsCounter.getAndClear();
if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) {
log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] 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()));
}
UUID scriptId = UUID.randomUUID();
pushedMsgs.incrementAndGet();
requestsCounter.increment();
return withTimeoutAndStatsCallback(scriptId, null,
doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
} else {
@ -141,7 +163,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t));
}
reportExecution(tenantId, customerId);
pushedMsgs.incrementAndGet();
requestsCounter.increment();
log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout());
var task = doInvokeFunction(scriptId, args);
@ -278,4 +300,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
private <T> ListenableFuture<T> error(String 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 lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.stats.StatsCounter;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@AllArgsConstructor
public class ScriptStatCallback<T> implements FutureCallback<T> {
private final AtomicInteger successMsgs;
private final AtomicInteger timeoutMsgs;
private final AtomicInteger failedMsgs;
private final StatsCounter successMsgs;
private final StatsCounter timeoutMsgs;
private final StatsCounter failedMsgs;
@Override
public void onSuccess(@Nullable T result) {
successMsgs.incrementAndGet();
successMsgs.increment();
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
timeoutMsgs.incrementAndGet();
timeoutMsgs.increment();
} 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.id.CustomerId;
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.TbApiUsageStateClient;
@ -117,4 +118,8 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic
.hash().toString();
}
@Override
protected StatsType getStatsType() {
return StatsType.JS_INVOKE;
}
}

6
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.id.CustomerId;
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.TbApiUsageStateClient;
@ -263,4 +264,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
protected long getMaxEvalRequestsTimeout() {
return maxInvokeRequestsTimeout * 2;
}
@Override
protected StatsType getStatsType() {
return StatsType.TBEL_INVOKE;
}
}

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

@ -18,8 +18,6 @@ package org.thingsboard.script.api.tbel;
import com.google.common.primitives.Bytes;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.mvel2.ConversionHandler;
import org.mvel2.DataConversion;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
@ -259,6 +257,8 @@ public class TbUtils {
float.class, int.class)));
parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt",
double.class)));
parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN",
double.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray",
@ -1163,6 +1163,10 @@ public class TbUtils {
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) {
return toFlatMap(ctx, json, new ArrayList<>(), true);
}

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

@ -1138,12 +1138,18 @@ public class TbUtilsTest {
}
@Test
void toInt() {
public void toInt() {
Assertions.assertEquals(1729, TbUtils.toInt(doubleVal));
Assertions.assertEquals(13, TbUtils.toInt(12.8));
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) {
List<Byte> result = new ArrayList<>(data.length);
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();
}
public int getAndClear() {
return aiCounter.getAndSet(0);
}
public void add(int delta){
aiCounter.addAndGet(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"),
TRANSPORT("transport"),
JS_INVOKE("jsInvoke"),
TBEL_INVOKE("tbelInvoke"),
RATE_EXECUTOR("rateExecutor"),
HOUSEKEEPER("housekeeper"),
EDGE("edge"),

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();
if (defaultConfig != null) {
updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks);
updated |= convertToImageUrls(tenantId, prefix, defaultConfig, imagesLinks);
widgetTypeDetails.setDefaultConfig(defaultConfig);
}
}

4
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -21,6 +21,7 @@ import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
@ -158,6 +159,9 @@ public abstract class DataValidator<D extends BaseData<?>> {
protected static void validateQueueName(String name) {
validateQueueNameOrTopic(name, NAME);
if (DataConstants.CF_QUEUE_NAME.equals(name) || DataConstants.CF_STATES_QUEUE_NAME.equals(name)) {
throw new DataValidationException(String.format("The queue name '%s' is not allowed. This name is reserved for internal use. Please choose a different name.", name));
}
}
protected static void validateQueueTopic(String topic) {

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")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT new org.thingsboard.server.common.data.edqs.fields.GenericFields(e.id, e.createdTime, e.tenantId," +
"e.name, e.version) FROM EntityViewEntity e WHERE e.id > :id ORDER BY e.id")
@Query("SELECT new org.thingsboard.server.common.data.edqs.fields.EntityViewFields(e.id, e.createdTime, e.tenantId, " +
"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);
}

73
docker/compose-utils.sh

@ -143,7 +143,6 @@ function additionalComposeEdqsArgs() {
function permissionList() {
PERMISSION_LIST="
799 799 tb-node/log
799 799 tb-transports/coap/log
799 799 tb-transports/lwm2m/log
799 799 tb-transports/http/log
799 799 tb-transports/mqtt/log
@ -200,29 +199,77 @@ function permissionList() {
}
function checkFolders() {
CREATE=false
SKIP_CHOWN=false
for i in "$@"
do
case $i in
--create)
CREATE=true
shift
;;
--skipChown)
SKIP_CHOWN=true
shift
;;
*)
# unknown option
;;
esac
done
EXIT_CODE=0
PERMISSION_LIST=$(permissionList) || exit $?
set -e
while read -r USR GRP DIR
do
if [ -z "$DIR" ]; then # skip empty lines
IS_EXIST_CHECK_PASSED=false
IS_OWNER_CHECK_PASSED=false
# skip empty lines
if [ -z "$DIR" ]; then
continue
fi
MESSAGE="Checking user ${USR} group ${GRP} dir ${DIR}"
if [[ -d "$DIR" ]] &&
[[ $(ls -ldn "$DIR" | awk '{print $3}') -eq "$USR" ]] &&
[[ $(ls -ldn "$DIR" | awk '{print $4}') -eq "$GRP" ]]
then
MESSAGE="$MESSAGE OK"
# checks section
echo "Checking if dir ${DIR} exists..."
if [[ -d "$DIR" ]]; then
echo "> OK"
IS_EXIST_CHECK_PASSED=true
if [ "$SKIP_CHOWN" = false ]; then
echo "Checking user ${USR} group ${GRP} ownership for dir ${DIR}..."
if [[ $(ls -ldn "$DIR" | awk '{print $3}') -eq "$USR" ]] && [[ $(ls -ldn "$DIR" | awk '{print $4}') -eq "$GRP" ]]; then
echo "> OK"
IS_OWNER_CHECK_PASSED=true
else
echo "...ownership check failed"
if [ "$CREATE" = false ]; then
EXIT_CODE=1
fi
fi
fi
else
if [ "$1" = "--create" ]; then
echo "Create and chown: user ${USR} group ${GRP} dir ${DIR}"
mkdir -p "$DIR" && sudo chown -R "$USR":"$GRP" "$DIR"
else
echo "$MESSAGE FAILED"
echo "...does not exist"
if [ "$CREATE" = false ]; then
EXIT_CODE=1
fi
fi
# create/chown section
if [ "$CREATE" = true ]; then
if [ "$IS_EXIST_CHECK_PASSED" = false ]; then
echo "...will create dir ${DIR}"
if [ "$SKIP_CHOWN" = false ]; then
echo "...will change ownership to user ${USR} group ${GRP} for dir ${DIR}"
mkdir -p "$DIR" && sudo chown -R "$USR":"$GRP" "$DIR" && echo "> OK"
else
mkdir -p "$DIR" && echo "> OK"
fi
elif [ "$IS_OWNER_CHECK_PASSED" = false ] && [ "$SKIP_CHOWN" = false ]; then
echo "...will change ownership to user ${USR} group ${GRP} for dir ${DIR}"
sudo chown -R "$USR":"$GRP" "$DIR" && echo "> OK"
fi
fi
done < <(echo "$PERMISSION_LIST")
return $EXIT_CODE
}

11
docker/docker-check-log-folders.sh

@ -17,5 +17,12 @@
set -e
source compose-utils.sh
checkFolders || exit $?
echo "OK"
if checkFolders "$@" ; then
echo "------"
echo "All checks have passed"
else
CHECK_EXIT_CODE=$?
echo "------"
echo "Some checks did not pass - check the output"
exit $CHECK_EXIT_CODE
fi

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

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

12
docker/docker-compose.edqs.yml

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

2
docker/docker-compose.yml

@ -20,7 +20,7 @@ version: '3.0'
services:
zookeeper:
restart: always
image: "zookeeper:3.8.0"
image: "zookeeper:3.8.1"
ports:
- "2181"
environment:

2
docker/docker-create-log-folders.sh

@ -17,4 +17,4 @@
set -e
source compose-utils.sh
checkFolders --create
checkFolders --create "$@"

2
docker/docker-install-tb.sh

@ -53,8 +53,6 @@ ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $?
ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
checkFolders --create || exit $?
if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then
COMPOSE_ARGS="\

2
docker/docker-start-services.sh

@ -31,8 +31,6 @@ ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $?
ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $?
checkFolders --create || exit $?
COMPOSE_ARGS="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS} ${ADDITIONAL_COMPOSE_MONITORING_ARGS} ${ADDITIONAL_COMPOSE_EDQS_ARGS} \
up -d"

2
docker/docker-upgrade-tb.sh

@ -46,8 +46,6 @@ ADDITIONAL_COMPOSE_EDQS_ARGS=$(additionalComposeEdqsArgs) || exit $?
ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
checkFolders --create || exit $?
COMPOSE_ARGS_PULL="\
-f docker-compose.yml ${ADDITIONAL_CACHE_ARGS} ${ADDITIONAL_COMPOSE_ARGS} ${ADDITIONAL_COMPOSE_QUEUE_ARGS}
${ADDITIONAL_COMPOSE_EDQS_ARGS} \

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

@ -223,8 +223,8 @@
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 12,
"w": 24,
"h": 10,
"w": 12,
"x": 0,
"y": 10
},
@ -303,6 +303,100 @@
"align": false,
"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,

0
docker/edqs.env → docker/tb-edqs.env

4
docker/edqs/conf/logback.xml → docker/tb-edqs/conf/logback.xml

@ -21,10 +21,10 @@
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/edqs/${TB_SERVICE_ID}/tb-edqs.log</file>
<file>/var/log/tb-edqs/${TB_SERVICE_ID}/tb-edqs.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<fileNamePattern>/var/log/tb-edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>

4
docker/edqs/conf/edqs.conf → docker/tb-edqs/conf/tb-edqs.conf

@ -14,9 +14,9 @@
# limitations under the License.
#
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export LOG_FILENAME=tb-edqs.out
export LOADER_PATH=/usr/share/edqs/conf
export LOADER_PATH=/usr/share/tb-edqs/conf

2
edqs/src/main/resources/edqs.yml

@ -70,8 +70,6 @@ queue:
stats:
# Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Statistics printing interval for EDQS
print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}"
kafka:
# Kafka Bootstrap nodes in "host:port" format

1
msa/tb/docker-cassandra/Dockerfile

@ -29,7 +29,6 @@ ENV CASSANDRA_DATA=/data/cassandra
ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver
ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard
ENV SPRING_DATASOURCE_USERNAME=${pkg.user}
ENV SPRING_DATASOURCE_PASSWORD=postgres
ENV CASSANDRA_HOST=localhost
ENV CASSANDRA_PORT=9042

1
msa/tb/docker-postgres/Dockerfile

@ -29,7 +29,6 @@ ENV PATH=$PATH:/usr/lib/postgresql/$PG_MAJOR/bin
ENV SPRING_DRIVER_CLASS_NAME=org.postgresql.Driver
ENV SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/thingsboard
ENV SPRING_DATASOURCE_USERNAME=${pkg.user}
ENV SPRING_DATASOURCE_PASSWORD=postgres
ENV PGLOG=/var/log/postgres

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -375,12 +375,6 @@ public interface TbContext {
ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script, String... argNames);
void logJsEvalRequest();
void logJsEvalResponse();
void logJsEvalFailure();
String getServiceId();
EventLoopGroup getSharedEventLoop();

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java

@ -69,10 +69,8 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfig
}
private ListenableFuture<TbAlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ctx.logJsEvalRequest();
ListenableFuture<JsonNode> asyncDetails = buildAlarmDetails(msg, alarm.getDetails());
return Futures.transform(asyncDetails, details -> {
ctx.logJsEvalResponse();
AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details);
if (result.isSuccessful()) {
return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm());

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -118,15 +118,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
ListenableFuture<JsonNode> asyncDetails;
boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails();
if (buildDetails) {
ctx.logJsEvalRequest();
asyncDetails = buildAlarmDetails(msg, null);
} else {
asyncDetails = Futures.immediateFuture(null);
}
ListenableFuture<Alarm> asyncAlarm = Futures.transform(asyncDetails, details -> {
if (buildDetails) {
ctx.logJsEvalResponse();
}
Alarm newAlarm;
if (msgAlarm != null) {
newAlarm = msgAlarm;
@ -147,15 +143,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
ListenableFuture<JsonNode> asyncDetails;
boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails();
if (buildDetails) {
ctx.logJsEvalRequest();
asyncDetails = buildAlarmDetails(msg, existingAlarm.getDetails());
} else {
asyncDetails = Futures.immediateFuture(null);
}
ListenableFuture<AlarmApiCallResult> asyncUpdated = Futures.transform(asyncDetails, details -> {
if (buildDetails) {
ctx.logJsEvalResponse();
}
if (msgAlarm != null) {
existingAlarm.setSeverity(msgAlarm.getSeverity());
existingAlarm.setPropagate(msgAlarm.isPropagate());

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -75,18 +75,15 @@ public class TbLogNode implements TbNode {
return;
}
ctx.logJsEvalRequest();
Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback<String>() {
@Override
public void onSuccess(@Nullable String result) {
ctx.logJsEvalResponse();
log.info(result);
ctx.tellSuccess(msg);
}
@Override
public void onFailure(Throwable t) {
ctx.logJsEvalResponse();
ctx.tellFailure(msg, t);
}
}, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -160,10 +160,8 @@ public class TbMsgGeneratorNode implements TbNode {
prevMsg = ctx.newMsg(queueName, TbMsg.EMPTY_STRING, originatorId, msg.getCustomerId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
}
if (initialized.get()) {
ctx.logJsEvalRequest();
return Futures.transformAsync(scriptEngine.executeGenerateAsync(prevMsg), generated -> {
log.trace("generate process response, generated {}, config {}", generated, config);
ctx.logJsEvalResponse();
prevMsg = ctx.newMsg(queueName, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData());
return Futures.immediateFuture(prevMsg);
}, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -60,15 +60,12 @@ public class TbJsFilterNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ctx.logJsEvalRequest();
withCallback(scriptEngine.executeFilterAsync(msg),
filterResult -> {
ctx.logJsEvalResponse();
ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE);
},
t -> {
ctx.tellFailure(msg, t);
ctx.logJsEvalFailure();
}, ctx.getDbCallbackExecutor());
}

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

Loading…
Cancel
Save