Browse Source

Merge branch 'rc' of github.com:thingsboard/thingsboard into performance

pull/13510/head
IrynaMatveieva 1 year ago
parent
commit
7fb228e85e
  1. 2
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 11
      application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg
  3. 7
      application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
  4. 116
      application/src/main/data/json/system/scada_symbols/conical-tank.svg
  5. 24
      application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
  6. 314
      application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
  7. 323
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  8. 23
      application/src/main/data/json/system/scada_symbols/elevated-tank.svg
  9. 10
      application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg
  10. 25
      application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg
  11. 139
      application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg
  12. 5
      application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
  13. 30
      application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg
  14. 22
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  15. 116
      application/src/main/data/json/system/scada_symbols/large-conical-tank.svg
  16. 22
      application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
  17. 26
      application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
  18. 22
      application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
  19. 22
      application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
  20. 7
      application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
  21. 185
      application/src/main/data/json/system/scada_symbols/left-heat-pump.svg
  22. 14
      application/src/main/data/json/system/scada_symbols/meter.svg
  23. 30
      application/src/main/data/json/system/scada_symbols/pool-hp.svg
  24. 93
      application/src/main/data/json/system/scada_symbols/pool.svg
  25. 7
      application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
  26. 185
      application/src/main/data/json/system/scada_symbols/right-heat-pump.svg
  27. 28
      application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg
  28. 362
      application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
  29. 362
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  30. 30
      application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg
  31. 14
      application/src/main/data/json/system/scada_symbols/small-left-meter.svg
  32. 26
      application/src/main/data/json/system/scada_symbols/small-meter.svg
  33. 24
      application/src/main/data/json/system/scada_symbols/small-right-center.svg
  34. 30
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  35. 30
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  36. 22
      application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
  37. 22
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  38. 22
      application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
  39. 26
      application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
  40. 20
      application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg
  41. 7
      application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
  42. 31
      application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg
  43. 5
      application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
  44. 23
      application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
  45. 30
      application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg
  46. 22
      application/src/main/data/json/system/scada_symbols/vertical-tank.svg
  47. 5
      application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg
  48. 6
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  49. 4
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  50. 3
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java
  51. 10
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  52. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  53. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  54. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  55. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  56. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  57. 12
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  58. 33
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  59. 74
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  60. 90
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  61. 9
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  62. 33
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  63. 7
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  64. 128
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  65. 25
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  66. 10
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  67. 3
      application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
  68. 4
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  69. 2
      application/src/main/resources/thingsboard.yml
  70. 70
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  71. 39
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  72. 25
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  73. 6
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  74. 40
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  75. 527
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  76. 34
      application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
  77. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  78. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java
  79. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java
  80. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java
  81. 11
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java
  82. 10
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java
  83. 17
      common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributesSaveResult.java
  84. 1
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  85. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java
  86. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  87. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  88. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
  89. 11
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
  90. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  91. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java
  92. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  93. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  94. 51
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java
  95. 3
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  96. 2
      common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java
  97. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java
  98. 27
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  99. 38
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  100. 10
      dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

2
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

@ -564,6 +564,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroValues": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {
@ -977,6 +978,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroValues": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {

11
application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="400" fill="none" version="1.1" viewBox="0 0 400 400"><tb:metadata xmlns=""><![CDATA[{ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="400" fill="none" version="1.1" viewBox="0 0 400 400"><tb:metadata xmlns=""><![CDATA[{
"title": "HP 3 phase voltage relay", "title": "HP 3 phase voltage relay",
"description": "Three phase voltage relay with various states and inications.", "description": "Three phase voltage relay with various states and indications.",
"searchTags": [ "searchTags": [
"energy", "energy",
"power", "power",
@ -39,7 +39,7 @@
}, },
{ {
"tag": "firstPhaseValue", "tag": "firstPhaseValue",
"stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.firstPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.firstPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -49,7 +49,7 @@
}, },
{ {
"tag": "secondPhaseValue", "tag": "secondPhaseValue",
"stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.secondPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.secondPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -59,12 +59,12 @@
}, },
{ {
"tag": "thirdPhaseValue", "tag": "thirdPhaseValue",
"stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.thirdPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.thirdPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -488,6 +488,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "V", "default": "V",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

7
application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -464,8 +464,7 @@
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "supportsUnitConversion": true
"visible": true
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

116
application/src/main/data/json/system/scada_symbols/conical-tank.svg

@ -159,96 +159,51 @@
"name": "Stand", "name": "Stand",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "tankColor", "id": "tankColor",
"name": "{i18n:scada.symbol.tank-color}", "name": "{i18n:scada.symbol.tank-color}",
"type": "color", "type": "color",
"default": "#E5E5E5", "default": "#E5E5E5",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "fluidColor", "id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}", "name": "{i18n:scada.symbol.fluid-color}",
"type": "color", "type": "color",
"default": "#1EC1F480", "default": "#1EC1F480",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBox", "id": "valueBox",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxColor", "id": "valueBoxColor",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"required": null,
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"divider": null, "supportsUnitConversion": true,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": "valueBox", "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -261,64 +216,35 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scale", "id": "scale",
"name": "{i18n:scada.symbol.scale}", "name": "{i18n:scada.symbol.scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "scale", "disableOnProperty": "scale",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 83 KiB

24
application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg

@ -33,7 +33,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -312,8 +312,8 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -368,6 +368,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -390,16 +398,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

314
application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg

@ -43,7 +43,7 @@
}, },
{ {
"tag": "maxValue", "tag": "maxValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -53,7 +53,7 @@
}, },
{ {
"tag": "minValue", "tag": "minValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -73,12 +73,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -329,16 +329,8 @@
"name": "{i18n:scada.symbol.min-max-value}", "name": "{i18n:scada.symbol.min-max-value}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "minValue", "id": "minValue",
@ -347,14 +339,9 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.min-value}", "subLabel": "{i18n:scada.symbol.min-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "maxValue", "id": "maxValue",
@ -363,30 +350,17 @@
"default": 100, "default": 100,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.max-value}", "subLabel": "{i18n:scada.symbol.max-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "value", "id": "value",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",
@ -395,14 +369,12 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "Decimals", "subLabel": "Decimals",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 10, "max": 10,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "valueFont", "id": "valueFont",
@ -415,64 +387,36 @@
"weight": "400", "weight": "400",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueColor", "id": "valueColor",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "color", "type": "color",
"default": "#002878", "default": "#002878",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "label", "id": "label",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelText", "id": "labelText",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "text", "type": "text",
"default": "Outdoor", "default": "Outdoor",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "",
"fieldClass": "flex", "fieldClass": "flex",
"min": null, "disabled": false,
"max": null, "visible": true
"step": null
}, },
{ {
"id": "labelFont", "id": "labelFont",
@ -485,64 +429,35 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelColor", "id": "labelColor",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showUnits", "id": "showUnits",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": "showUnits",
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsFont", "id": "unitsFont",
@ -555,224 +470,119 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsColor", "id": "unitsColor",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.arrow-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showTarget", "id": "showTarget",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "targetColor", "id": "targetColor",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "color", "type": "color",
"default": "#DEDEDE", "default": "#DEDEDE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showTarget", "disableOnProperty": "showTarget",
"rowClass": "", "disabled": true,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showHighCriticalScale", "id": "showHighCriticalScale",
"name": "{i18n:scada.symbol.show-high-critical-scale}", "name": "{i18n:scada.symbol.show-high-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showHighWarningScale", "id": "showHighWarningScale",
"name": "{i18n:scada.symbol.show-high-warning-scale}", "name": "{i18n:scada.symbol.show-high-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showLowWarningScale", "id": "showLowWarningScale",
"name": "{i18n:scada.symbol.show-low-warning-scale}", "name": "{i18n:scada.symbol.show-low-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showLowCriticalScale", "id": "showLowCriticalScale",
"name": "{i18n:scada.symbol.show-low-critical-scale}", "name": "{i18n:scada.symbol.show-low-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scaleColor", "id": "scaleColor",
"name": "{i18n:scada.symbol.scale-color}", "name": "{i18n:scada.symbol.scale-color}",
"type": "color", "type": "color",
"default": "#C8DFF7", "default": "#C8DFF7",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultWarningScaleColor", "id": "defaultWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#EBEBEB", "default": "#EBEBEB",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeWarningScaleColor", "id": "activeWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultCriticalScaleColor", "id": "defaultCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeCriticalScaleColor", "id": "activeCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 26 KiB

323
application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg

@ -43,7 +43,7 @@
}, },
{ {
"tag": "maxValue", "tag": "maxValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -53,7 +53,7 @@
}, },
{ {
"tag": "minValue", "tag": "minValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -73,12 +73,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -329,16 +329,8 @@
"name": "{i18n:scada.symbol.min-max-value}", "name": "{i18n:scada.symbol.min-max-value}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "minValue", "id": "minValue",
@ -347,14 +339,9 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.min-value}", "subLabel": "{i18n:scada.symbol.min-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "maxValue", "id": "maxValue",
@ -363,30 +350,17 @@
"default": 100, "default": 100,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.max-value}", "subLabel": "{i18n:scada.symbol.max-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "value", "id": "value",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",
@ -395,14 +369,11 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "Decimals", "subLabel": "Decimals",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 10, "max": 10,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "valueFont", "id": "valueFont",
@ -415,64 +386,33 @@
"weight": "400", "weight": "400",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueColor", "id": "valueColor",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "color", "type": "color",
"default": "#002878", "default": "#002878",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "label", "id": "label",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelText", "id": "labelText",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "text", "type": "text",
"default": "Outdoor", "default": "Outdoor",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "flex", "fieldClass": "flex",
"min": null, "disabled": false,
"max": null, "visible": true
"step": null
}, },
{ {
"id": "labelFont", "id": "labelFont",
@ -485,64 +425,34 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelColor", "id": "labelColor",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showUnits", "id": "showUnits",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "required": false,
"subLabel": null, "supportsUnitConversion": true,
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsFont", "id": "unitsFont",
@ -555,224 +465,117 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsColor", "id": "unitsColor",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.arrow-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showTarget", "id": "showTarget",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "targetColor", "id": "targetColor",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "color", "type": "color",
"default": "#DEDEDE", "default": "#DEDEDE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showTarget", "disableOnProperty": "showTarget",
"rowClass": "", "disabled": true,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showHighCriticalScale", "id": "showHighCriticalScale",
"name": "{i18n:scada.symbol.show-high-critical-scale}", "name": "{i18n:scada.symbol.show-high-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showHighWarningScale", "id": "showHighWarningScale",
"name": "{i18n:scada.symbol.show-high-warning-scale}", "name": "{i18n:scada.symbol.show-high-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showLowWarningScale", "id": "showLowWarningScale",
"name": "{i18n:scada.symbol.show-low-warning-scale}", "name": "{i18n:scada.symbol.show-low-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showLowCriticalScale", "id": "showLowCriticalScale",
"name": "{i18n:scada.symbol.show-low-critical-scale}", "name": "{i18n:scada.symbol.show-low-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scaleColor", "id": "scaleColor",
"name": "{i18n:scada.symbol.scale-color}", "name": "{i18n:scada.symbol.scale-color}",
"type": "color", "type": "color",
"default": "#C8DFF7", "default": "#C8DFF7",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultWarningScaleColor", "id": "defaultWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#EBEBEB", "default": "#EBEBEB",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeWarningScaleColor", "id": "activeWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultCriticalScaleColor", "id": "defaultCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeCriticalScaleColor", "id": "activeCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 25 KiB

23
application/src/main/data/json/system/scada_symbols/elevated-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -308,7 +308,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -373,6 +373,15 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -386,16 +395,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

10
application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg

@ -38,12 +38,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.measured, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.measured, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -353,6 +353,7 @@
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "text", "type": "text",
"default": "T1", "default": "T1",
"disableOnProperty": "showLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -368,6 +369,7 @@
"weight": "400", "weight": "400",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -376,6 +378,7 @@
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -392,6 +395,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "kWh", "default": "kWh",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -406,6 +410,7 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -414,6 +419,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

25
application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "export-rate", "tag": "export-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.exportValueFont, ctx.properties.exportValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.exportRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.exportValueFont, ctx.properties.exportValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.exportRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -48,7 +48,7 @@
}, },
{ {
"tag": "night-rate", "tag": "night-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -58,7 +58,7 @@
}, },
{ {
"tag": "off-peak-rate", "tag": "off-peak-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -68,12 +68,12 @@
}, },
{ {
"tag": "peak-rate", "tag": "peak-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -533,6 +533,7 @@
"group": "{i18n:scada.symbol.off-peak-rate}", "group": "{i18n:scada.symbol.off-peak-rate}",
"type": "text", "type": "text",
"default": "T1", "default": "T1",
"disableOnProperty": "showOffPeakLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -549,6 +550,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showOffPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -558,6 +560,7 @@
"group": "{i18n:scada.symbol.off-peak-rate}", "group": "{i18n:scada.symbol.off-peak-rate}",
"type": "color", "type": "color",
"default": "#000000", "default": "#000000",
"disableOnProperty": "showOffPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -609,6 +612,7 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "text", "type": "text",
"default": "T2", "default": "T2",
"disableOnProperty": "showNightLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -625,6 +629,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showNightLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -634,6 +639,7 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showNightLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -685,6 +691,7 @@
"group": "{i18n:scada.symbol.peak-rate}", "group": "{i18n:scada.symbol.peak-rate}",
"type": "text", "type": "text",
"default": "T3", "default": "T3",
"disableOnProperty": "showPeakLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -701,6 +708,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -710,6 +718,7 @@
"group": "{i18n:scada.symbol.peak-rate}", "group": "{i18n:scada.symbol.peak-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -761,6 +770,7 @@
"group": "{i18n:scada.symbol.export-rate}", "group": "{i18n:scada.symbol.export-rate}",
"type": "text", "type": "text",
"default": "Export", "default": "Export",
"disableOnProperty": "showExportLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -777,6 +787,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showExportLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -786,6 +797,7 @@
"group": "{i18n:scada.symbol.export-rate}", "group": "{i18n:scada.symbol.export-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showExportLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -835,6 +847,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "kWh", "default": "kWh",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -849,6 +862,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -857,6 +871,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
} }

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

139
application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg

@ -62,7 +62,7 @@
}, },
{ {
"tag": "value-text", "tag": "value-text",
"stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);",
"actions": null "actions": null
}, },
{ {
@ -366,80 +366,49 @@
"name": "{i18n:scada.symbol.colors}", "name": "{i18n:scada.symbol.colors}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null,
"subLabel": "{i18n:scada.symbol.running}", "subLabel": "{i18n:scada.symbol.running}",
"divider": true, "divider": true,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "stoppedColor", "id": "stoppedColor",
"name": "{i18n:scada.symbol.colors}", "name": "{i18n:scada.symbol.colors}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null,
"subLabel": "{i18n:scada.symbol.stopped}", "subLabel": "{i18n:scada.symbol.stopped}",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "warningColor", "id": "warningColor",
"name": "{i18n:scada.symbol.alarm-colors}", "name": "{i18n:scada.symbol.alarm-colors}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null,
"subLabel": "{i18n:scada.symbol.warning}", "subLabel": "{i18n:scada.symbol.warning}",
"divider": true, "divider": true,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "criticalColor", "id": "criticalColor",
"name": "{i18n:scada.symbol.alarm-colors}", "name": "{i18n:scada.symbol.alarm-colors}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null,
"subLabel": "{i18n:scada.symbol.critical}", "subLabel": "{i18n:scada.symbol.critical}",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "minTemperature", "id": "minTemperature",
"name": "{i18n:scada.symbol.temperature}", "name": "{i18n:scada.symbol.temperature}",
"type": "number", "type": "number",
"default": 10, "default": 10,
"required": null,
"subLabel": "Min", "subLabel": "Min",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "column-xs", "rowClass": "column-xs",
"fieldClass": "", "step": 1,
"min": null, "disabled": false,
"max": null, "visible": true
"step": 1
}, },
{ {
"id": "maxTemperature", "id": "maxTemperature",
@ -448,14 +417,9 @@
"default": 45, "default": 45,
"required": true, "required": true,
"subLabel": "Max", "subLabel": "Max",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "temperatureStep", "id": "temperatureStep",
@ -463,31 +427,17 @@
"type": "number", "type": "number",
"default": 1, "default": 1,
"required": true, "required": true,
"subLabel": null, "step": 0.5,
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 0.5
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#002878", "default": "#002878",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -500,64 +450,33 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxBackground", "id": "valueBoxBackground",
"name": "{i18n:scada.symbol.value-box-background}", "name": "{i18n:scada.symbol.value-box-background}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

5
application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -464,6 +464,7 @@
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

30
application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -346,6 +346,24 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": false,
"supportsUnitConversion": true,
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -359,16 +377,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

22
application/src/main/data/json/system/scada_symbols/horizontal-tank.svg

@ -33,7 +33,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -312,7 +312,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -368,6 +368,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -390,16 +398,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

116
application/src/main/data/json/system/scada_symbols/large-conical-tank.svg

@ -160,96 +160,51 @@
"name": "Stand", "name": "Stand",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "tankColor", "id": "tankColor",
"name": "{i18n:scada.symbol.tank-color}", "name": "{i18n:scada.symbol.tank-color}",
"type": "color", "type": "color",
"default": "#E5E5E5", "default": "#E5E5E5",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "fluidColor", "id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}", "name": "{i18n:scada.symbol.fluid-color}",
"type": "color", "type": "color",
"default": "#1EC1F480", "default": "#1EC1F480",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBox", "id": "valueBox",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxColor", "id": "valueBoxColor",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"required": null,
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"divider": null, "supportsUnitConversion": true,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": "valueBox", "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -262,64 +217,35 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scale", "id": "scale",
"name": "{i18n:scada.symbol.scale}", "name": "{i18n:scada.symbol.scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "scale", "disableOnProperty": "scale",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

22
application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg

@ -33,7 +33,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -312,7 +312,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -368,6 +368,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -390,16 +398,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

26
application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -365,7 +365,17 @@
"value": false, "value": false,
"label": "Absolute" "label": "Absolute"
} }
] ],
"disabled": false,
"visible": true
},
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
}, },
{ {
"id": "transparent", "id": "transparent",
@ -389,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

22
application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

22
application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg

@ -33,7 +33,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -312,7 +312,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -368,6 +368,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -390,16 +398,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

7
application/src/main/data/json/system/scada_symbols/left-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,7 +463,8 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width" "fieldClass": "medium-width",
"supportsUnitConversion": true
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

185
application/src/main/data/json/system/scada_symbols/left-heat-pump.svg

@ -72,7 +72,7 @@
}, },
{ {
"tag": "value-text", "tag": "value-text",
"stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);",
"actions": null "actions": null
} }
], ],
@ -432,13 +432,11 @@
"required": true, "required": true,
"subLabel": "Min", "subLabel": "Min",
"divider": true, "divider": true,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "column-xs", "rowClass": "column-xs",
"fieldClass": "",
"min": 0, "min": 0,
"max": null, "step": 1,
"step": 1 "disabled": false,
"visible": true
}, },
{ {
"id": "maxTemperature", "id": "maxTemperature",
@ -447,14 +445,10 @@
"default": 45, "default": 45,
"required": true, "required": true,
"subLabel": "Max", "subLabel": "Max",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": null, "step": 1,
"step": 1 "disabled": false,
"visible": true
}, },
{ {
"id": "temperatureStep", "id": "temperatureStep",
@ -462,31 +456,17 @@
"type": "number", "type": "number",
"default": 1, "default": 1,
"required": true, "required": true,
"subLabel": null, "step": 0.5,
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 0.5
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#000000C2", "default": "#000000C2",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -499,192 +479,101 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxBackground", "id": "valueBoxBackground",
"name": "{i18n:scada.symbol.value-box-background}", "name": "{i18n:scada.symbol.value-box-background}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "powerButtonBackground", "id": "powerButtonBackground",
"name": "{i18n:scada.symbol.power-button-background}", "name": "{i18n:scada.symbol.power-button-background}",
"type": "color", "type": "color",
"default": "#1ABB48", "default": "#1ABB48",
"required": null,
"subLabel": "Enabled", "subLabel": "Enabled",
"divider": true, "divider": true,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "column-xs", "rowClass": "column-xs",
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": null
}, },
{ {
"id": "disabledPowerButtonBackground", "id": "disabledPowerButtonBackground",
"name": "{i18n:scada.symbol.power-button-background}", "name": "{i18n:scada.symbol.power-button-background}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null,
"subLabel": "Disabled", "subLabel": "Disabled",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "runningColor", "id": "runningColor",
"name": "{i18n:scada.symbol.running-color}", "name": "{i18n:scada.symbol.running-color}",
"type": "color", "type": "color",
"default": "#1C943E", "default": "#1C943E",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "stoppedColor", "id": "stoppedColor",
"name": "{i18n:scada.symbol.stopped-color}", "name": "{i18n:scada.symbol.stopped-color}",
"type": "color", "type": "color",
"default": "#696969", "default": "#696969",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "warningColor", "id": "warningColor",
"name": "{i18n:scada.symbol.warning-color}", "name": "{i18n:scada.symbol.warning-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "criticalColor", "id": "criticalColor",
"name": "{i18n:scada.symbol.critical-color}", "name": "{i18n:scada.symbol.critical-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "heatPumpColor", "id": "heatPumpColor",
"name": "{i18n:scada.symbol.heat-pump-color}", "name": "{i18n:scada.symbol.heat-pump-color}",
"type": "color", "type": "color",
"default": "#E5E5E5", "default": "#E5E5E5",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "pipeColor", "id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}", "name": "{i18n:scada.symbol.pipe-color}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 54 KiB

14
application/src/main/data/json/system/scada_symbols/meter.svg

@ -51,7 +51,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 328 : 365;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 328 : 365;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -433,7 +433,7 @@
"type": "units", "type": "units",
"default": "%", "default": "%",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -493,6 +493,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -510,7 +518,7 @@
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
"type": "font", "type": "font",
"default": { "default": {
"size": 12, "size": 10,
"sizeUnit": "px", "sizeUnit": "px",
"family": "Roboto", "family": "Roboto",
"weight": "500", "weight": "500",

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

30
application/src/main/data/json/system/scada_symbols/pool-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -346,6 +346,24 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": false,
"supportsUnitConversion": true,
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -394,16 +412,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "minorColor", "id": "minorColor",
"name": "{i18n:scada.symbol.minor-ticks}", "name": "{i18n:scada.symbol.minor-ticks}",

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

93
application/src/main/data/json/system/scada_symbols/pool.svg

@ -148,80 +148,43 @@
"name": "{i18n:scada.symbol.tank-color}", "name": "{i18n:scada.symbol.tank-color}",
"type": "color", "type": "color",
"default": "#E5E5E5", "default": "#E5E5E5",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "fluidColor", "id": "fluidColor",
"name": "{i18n:scada.symbol.fluid-color}", "name": "{i18n:scada.symbol.fluid-color}",
"type": "color", "type": "color",
"default": "#1EC1F480", "default": "#1EC1F480",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBox", "id": "valueBox",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxColor", "id": "valueBoxColor",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"required": null,
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"divider": null, "supportsUnitConversion": true,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": "valueBox", "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -234,48 +197,26 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

7
application/src/main/data/json/system/scada_symbols/right-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -463,7 +463,8 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width" "fieldClass": "medium-width",
"supportsUnitConversion": true
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 73 KiB

185
application/src/main/data/json/system/scada_symbols/right-heat-pump.svg

@ -72,7 +72,7 @@
}, },
{ {
"tag": "value-text", "tag": "value-text",
"stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);",
"actions": null "actions": null
} }
], ],
@ -432,13 +432,11 @@
"required": true, "required": true,
"subLabel": "Min", "subLabel": "Min",
"divider": true, "divider": true,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "column-xs", "rowClass": "column-xs",
"fieldClass": "",
"min": 0, "min": 0,
"max": null, "step": 1,
"step": 1 "disabled": false,
"visible": true
}, },
{ {
"id": "maxTemperature", "id": "maxTemperature",
@ -447,14 +445,10 @@
"default": 45, "default": 45,
"required": true, "required": true,
"subLabel": "Max", "subLabel": "Max",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": null, "step": 1,
"step": 1 "disabled": false,
"visible": true
}, },
{ {
"id": "temperatureStep", "id": "temperatureStep",
@ -462,31 +456,17 @@
"type": "number", "type": "number",
"default": 1, "default": 1,
"required": true, "required": true,
"subLabel": null, "step": 0.5,
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 0.5
}, },
{ {
"id": "valueTextColor", "id": "valueTextColor",
"name": "{i18n:scada.symbol.value-text}", "name": "{i18n:scada.symbol.value-text}",
"type": "color", "type": "color",
"default": "#000000C2", "default": "#000000C2",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueTextFont", "id": "valueTextFont",
@ -499,192 +479,101 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueUnits", "id": "valueUnits",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.value-units}", "name": "{i18n:scada.symbol.value-units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueBoxBackground", "id": "valueBoxBackground",
"name": "{i18n:scada.symbol.value-box-background}", "name": "{i18n:scada.symbol.value-box-background}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "powerButtonBackground", "id": "powerButtonBackground",
"name": "{i18n:scada.symbol.power-button-background}", "name": "{i18n:scada.symbol.power-button-background}",
"type": "color", "type": "color",
"default": "#1ABB48", "default": "#1ABB48",
"required": null,
"subLabel": "Enabled", "subLabel": "Enabled",
"divider": true, "divider": true,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "column-xs", "rowClass": "column-xs",
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": null
}, },
{ {
"id": "disabledPowerButtonBackground", "id": "disabledPowerButtonBackground",
"name": "{i18n:scada.symbol.power-button-background}", "name": "{i18n:scada.symbol.power-button-background}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null,
"subLabel": "Disabled", "subLabel": "Disabled",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "runningColor", "id": "runningColor",
"name": "{i18n:scada.symbol.running-color}", "name": "{i18n:scada.symbol.running-color}",
"type": "color", "type": "color",
"default": "#1C943E", "default": "#1C943E",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "stoppedColor", "id": "stoppedColor",
"name": "{i18n:scada.symbol.stopped-color}", "name": "{i18n:scada.symbol.stopped-color}",
"type": "color", "type": "color",
"default": "#696969", "default": "#696969",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "warningColor", "id": "warningColor",
"name": "{i18n:scada.symbol.warning-color}", "name": "{i18n:scada.symbol.warning-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "criticalColor", "id": "criticalColor",
"name": "{i18n:scada.symbol.critical-color}", "name": "{i18n:scada.symbol.critical-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "heatPumpColor", "id": "heatPumpColor",
"name": "{i18n:scada.symbol.heat-pump-color}", "name": "{i18n:scada.symbol.heat-pump-color}",
"type": "color", "type": "color",
"default": "#E5E5E5", "default": "#E5E5E5",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "pipeColor", "id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}", "name": "{i18n:scada.symbol.pipe-color}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#FFFFFF",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 54 KiB

28
application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -346,6 +346,22 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": false,
"supportsUnitConversion": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -359,16 +375,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

362
application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg

@ -43,7 +43,7 @@
}, },
{ {
"tag": "maxValue", "tag": "maxValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -53,7 +53,7 @@
}, },
{ {
"tag": "minValue", "tag": "minValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -73,12 +73,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -193,16 +193,8 @@
"name": "{i18n:scada.symbol.min-max-value}", "name": "{i18n:scada.symbol.min-max-value}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "minValue", "id": "minValue",
@ -211,14 +203,9 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.min-value}", "subLabel": "{i18n:scada.symbol.min-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "maxValue", "id": "maxValue",
@ -227,158 +214,89 @@
"default": 100, "default": 100,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.max-value}", "subLabel": "{i18n:scada.symbol.max-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "showHighCriticalScale", "id": "showHighCriticalScale",
"name": "{i18n:scada.symbol.high-critical-scale}", "name": "{i18n:scada.symbol.high-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "highCriticalScale", "id": "highCriticalScale",
"name": "{i18n:scada.symbol.high-critical-scale}", "name": "{i18n:scada.symbol.high-critical-scale}",
"type": "number", "type": "number",
"default": 85, "default": 85,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showHighCriticalScale", "disableOnProperty": "showHighCriticalScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showHighWarningScale", "id": "showHighWarningScale",
"name": "{i18n:scada.symbol.high-warning-scale}", "name": "{i18n:scada.symbol.high-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "highWarningScale", "id": "highWarningScale",
"name": "{i18n:scada.symbol.high-warning-scale}", "name": "{i18n:scada.symbol.high-warning-scale}",
"type": "number", "type": "number",
"default": 70, "default": 70,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showHighWarningScale", "disableOnProperty": "showHighWarningScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showLowWarningScale", "id": "showLowWarningScale",
"name": "{i18n:scada.symbol.low-warning-scale}", "name": "{i18n:scada.symbol.low-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "lowWarningScale", "id": "lowWarningScale",
"name": "{i18n:scada.symbol.low-warning-scale}", "name": "{i18n:scada.symbol.low-warning-scale}",
"type": "number", "type": "number",
"default": 30, "default": 30,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showLowWarningScale", "disableOnProperty": "showLowWarningScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showLowCriticalScale", "id": "showLowCriticalScale",
"name": "{i18n:scada.symbol.low-critical-scale}", "name": "{i18n:scada.symbol.low-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "lowCriticalScale", "id": "lowCriticalScale",
"name": "{i18n:scada.symbol.low-critical-scale}", "name": "{i18n:scada.symbol.low-critical-scale}",
"type": "number", "type": "number",
"default": 15, "default": 15,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showLowCriticalScale", "disableOnProperty": "showLowCriticalScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "value", "id": "value",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",
@ -387,14 +305,12 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "Decimals", "subLabel": "Decimals",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 10, "max": 10,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "valueFont", "id": "valueFont",
@ -407,64 +323,36 @@
"weight": "400", "weight": "400",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueColor", "id": "valueColor",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "color", "type": "color",
"default": "#002878", "default": "#002878",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "label", "id": "label",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelText", "id": "labelText",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "text", "type": "text",
"default": "Outdoor", "default": "Outdoor",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "",
"fieldClass": "flex", "fieldClass": "flex",
"min": null, "disabled": false,
"max": null, "visible": true
"step": null
}, },
{ {
"id": "labelFont", "id": "labelFont",
@ -477,64 +365,35 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelColor", "id": "labelColor",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showUnits", "id": "showUnits",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": "showUnits",
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsFont", "id": "unitsFont",
@ -547,160 +406,87 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsColor", "id": "unitsColor",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.arrow-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showTarget", "id": "showTarget",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "targetColor", "id": "targetColor",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "color", "type": "color",
"default": "#DEDEDE", "default": "#DEDEDE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showTarget", "disableOnProperty": "showTarget",
"rowClass": "", "disabled": true,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scaleColor", "id": "scaleColor",
"name": "{i18n:scada.symbol.scale-color}", "name": "{i18n:scada.symbol.scale-color}",
"type": "color", "type": "color",
"default": "#C8DFF7", "default": "#C8DFF7",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultWarningScaleColor", "id": "defaultWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#EBEBEB", "default": "#EBEBEB",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeWarningScaleColor", "id": "activeWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultCriticalScaleColor", "id": "defaultCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeCriticalScaleColor", "id": "activeCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB

362
application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg

@ -43,7 +43,7 @@
}, },
{ {
"tag": "maxValue", "tag": "maxValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -53,7 +53,7 @@
}, },
{ {
"tag": "minValue", "tag": "minValue",
"stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}",
"actions": null "actions": null
}, },
{ {
@ -73,12 +73,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -193,16 +193,8 @@
"name": "{i18n:scada.symbol.min-max-value}", "name": "{i18n:scada.symbol.min-max-value}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "minValue", "id": "minValue",
@ -211,14 +203,9 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.min-value}", "subLabel": "{i18n:scada.symbol.min-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "maxValue", "id": "maxValue",
@ -227,158 +214,89 @@
"default": 100, "default": 100,
"required": true, "required": true,
"subLabel": "{i18n:scada.symbol.max-value}", "subLabel": "{i18n:scada.symbol.max-value}",
"divider": null, "step": 1,
"fieldSuffix": null, "disabled": false,
"disableOnProperty": null, "visible": true
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": 1
}, },
{ {
"id": "showHighCriticalScale", "id": "showHighCriticalScale",
"name": "{i18n:scada.symbol.high-critical-scale}", "name": "{i18n:scada.symbol.high-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "highCriticalScale", "id": "highCriticalScale",
"name": "{i18n:scada.symbol.high-critical-scale}", "name": "{i18n:scada.symbol.high-critical-scale}",
"type": "number", "type": "number",
"default": 85, "default": 85,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showHighCriticalScale", "disableOnProperty": "showHighCriticalScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showHighWarningScale", "id": "showHighWarningScale",
"name": "{i18n:scada.symbol.high-warning-scale}", "name": "{i18n:scada.symbol.high-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "highWarningScale", "id": "highWarningScale",
"name": "{i18n:scada.symbol.high-warning-scale}", "name": "{i18n:scada.symbol.high-warning-scale}",
"type": "number", "type": "number",
"default": 70, "default": 70,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showHighWarningScale", "disableOnProperty": "showHighWarningScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showLowWarningScale", "id": "showLowWarningScale",
"name": "{i18n:scada.symbol.low-warning-scale}", "name": "{i18n:scada.symbol.low-warning-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "lowWarningScale", "id": "lowWarningScale",
"name": "{i18n:scada.symbol.low-warning-scale}", "name": "{i18n:scada.symbol.low-warning-scale}",
"type": "number", "type": "number",
"default": 30, "default": 30,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showLowWarningScale", "disableOnProperty": "showLowWarningScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "showLowCriticalScale", "id": "showLowCriticalScale",
"name": "{i18n:scada.symbol.low-critical-scale}", "name": "{i18n:scada.symbol.low-critical-scale}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "lowCriticalScale", "id": "lowCriticalScale",
"name": "{i18n:scada.symbol.low-critical-scale}", "name": "{i18n:scada.symbol.low-critical-scale}",
"type": "number", "type": "number",
"default": 15, "default": 15,
"required": null,
"subLabel": "",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showLowCriticalScale", "disableOnProperty": "showLowCriticalScale",
"rowClass": "", "step": 1,
"fieldClass": "", "disabled": false,
"min": null, "visible": true
"max": null,
"step": 1
}, },
{ {
"id": "value", "id": "value",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",
@ -387,14 +305,12 @@
"default": 0, "default": 0,
"required": true, "required": true,
"subLabel": "Decimals", "subLabel": "Decimals",
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "",
"fieldClass": "",
"min": 0, "min": 0,
"max": 10, "max": 10,
"step": 1 "step": 1,
"disabled": false,
"visible": true
}, },
{ {
"id": "valueFont", "id": "valueFont",
@ -407,64 +323,36 @@
"weight": "400", "weight": "400",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "valueColor", "id": "valueColor",
"name": "{i18n:scada.symbol.value}", "name": "{i18n:scada.symbol.value}",
"type": "color", "type": "color",
"default": "#002878", "default": "#002878",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "value", "disableOnProperty": "value",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "label", "id": "label",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelText", "id": "labelText",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "text", "type": "text",
"default": "Outdoor", "default": "Outdoor",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "",
"fieldClass": "flex", "fieldClass": "flex",
"min": null, "disabled": false,
"max": null, "visible": true
"step": null
}, },
{ {
"id": "labelFont", "id": "labelFont",
@ -477,64 +365,35 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "labelColor", "id": "labelColor",
"name": "{i18n:scada.symbol.label}", "name": "{i18n:scada.symbol.label}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "label", "disableOnProperty": "label",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showUnits", "id": "showUnits",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "switch", "type": "switch",
"default": true, "default": true,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "units", "id": "units",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "°C", "default": "°C",
"required": null, "supportsUnitConversion": true,
"subLabel": null, "disabled": false,
"divider": null, "visible": true
"fieldSuffix": null,
"disableOnProperty": "showUnits",
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsFont", "id": "unitsFont",
@ -547,160 +406,87 @@
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, },
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "unitsColor", "id": "unitsColor",
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000000DE", "default": "#000000DE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showUnits", "disableOnProperty": "showUnits",
"rowClass": "", "disabled": false,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "arrowColor", "id": "arrowColor",
"name": "{i18n:scada.symbol.arrow-color}", "name": "{i18n:scada.symbol.arrow-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "showTarget", "id": "showTarget",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "switch", "type": "switch",
"default": false, "default": false,
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "targetColor", "id": "targetColor",
"name": "{i18n:scada.symbol.target}", "name": "{i18n:scada.symbol.target}",
"type": "color", "type": "color",
"default": "#DEDEDE", "default": "#DEDEDE",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": "showTarget", "disableOnProperty": "showTarget",
"rowClass": "", "disabled": true,
"fieldClass": "", "visible": true
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "scaleColor", "id": "scaleColor",
"name": "{i18n:scada.symbol.scale-color}", "name": "{i18n:scada.symbol.scale-color}",
"type": "color", "type": "color",
"default": "#C8DFF7", "default": "#C8DFF7",
"required": null, "disabled": false,
"subLabel": null, "visible": true
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultWarningScaleColor", "id": "defaultWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#EBEBEB", "default": "#EBEBEB",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeWarningScaleColor", "id": "activeWarningScaleColor",
"name": "{i18n:scada.symbol.warning-scale-color}", "name": "{i18n:scada.symbol.warning-scale-color}",
"type": "color", "type": "color",
"default": "#FAA405", "default": "#FAA405",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "defaultCriticalScaleColor", "id": "defaultCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#666666", "default": "#666666",
"required": null,
"subLabel": "Default", "subLabel": "Default",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}, },
{ {
"id": "activeCriticalScaleColor", "id": "activeCriticalScaleColor",
"name": "{i18n:scada.symbol.critical-scale-color}", "name": "{i18n:scada.symbol.critical-scale-color}",
"type": "color", "type": "color",
"default": "#D12730", "default": "#D12730",
"required": null,
"subLabel": "Active", "subLabel": "Active",
"divider": null, "disabled": false,
"fieldSuffix": null, "visible": true
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
} }
] ]
}]]></tb:metadata> }]]></tb:metadata>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB

30
application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -304,7 +304,7 @@
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -313,8 +313,8 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -329,7 +329,7 @@
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -338,7 +338,7 @@
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

14
application/src/main/data/json/system/scada_symbols/small-left-meter.svg

@ -51,7 +51,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(50, end+11, 50, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(38, y, 50, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 32, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(44, minorY, 50, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(50, end+11, 50, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(38, y, 50, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 32, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(44, minorY, 50, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -433,7 +433,7 @@
"type": "units", "type": "units",
"default": "%", "default": "%",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -493,6 +493,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -510,7 +518,7 @@
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
"type": "font", "type": "font",
"default": { "default": {
"size": 12, "size": 10,
"sizeUnit": "px", "sizeUnit": "px",
"family": "Roboto", "family": "Roboto",
"weight": "500", "weight": "500",

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

26
application/src/main/data/json/system/scada_symbols/small-meter.svg

@ -51,7 +51,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 132 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 132 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -424,7 +424,7 @@
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -433,8 +433,8 @@
"type": "units", "type": "units",
"default": "%", "default": "%",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -449,7 +449,7 @@
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -490,7 +490,15 @@
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true
},
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true "visible": true
}, },
{ {
@ -510,14 +518,12 @@
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
"type": "font", "type": "font",
"default": { "default": {
"size": 12, "size": 10,
"sizeUnit": "px", "sizeUnit": "px",
"family": "Roboto", "family": "Roboto",
"weight": "500", "weight": "500",
"style": "normal" "style": "normal"
}, }
"disabled": false,
"visible": true
}, },
{ {
"id": "majorColor", "id": "majorColor",

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

24
application/src/main/data/json/system/scada_symbols/small-right-center.svg

@ -51,7 +51,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(73, end+11, 73, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(61, y, 73, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 55, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(67, minorY, 73, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(73, end+11, 73, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(61, y, 73, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 55, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(67, minorY, 73, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -375,7 +375,7 @@
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
}, },
"disableOnProperty": "progressArrow", "disableOnProperty": "progressArrow",
"disabled": false, "disabled": true,
"visible": true "visible": true
}, },
{ {
@ -424,7 +424,7 @@
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -433,8 +433,8 @@
"type": "units", "type": "units",
"default": "%", "default": "%",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -449,7 +449,7 @@
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -490,7 +490,15 @@
"colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true
},
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true "visible": true
}, },
{ {
@ -510,7 +518,7 @@
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
"type": "font", "type": "font",
"default": { "default": {
"size": 12, "size": 10,
"sizeUnit": "px", "sizeUnit": "px",
"family": "Roboto", "family": "Roboto",
"weight": "500", "weight": "500",

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

30
application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -304,7 +304,7 @@
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -313,8 +313,8 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -329,7 +329,7 @@
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -338,7 +338,7 @@
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

30
application/src/main/data/json/system/scada_symbols/spherical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -304,7 +304,7 @@
"type": "color", "type": "color",
"default": "#F3F3F3", "default": "#F3F3F3",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -313,8 +313,8 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -329,7 +329,7 @@
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -338,7 +338,7 @@
"type": "color", "type": "color",
"default": "#0000008A", "default": "#0000008A",
"disableOnProperty": "valueBox", "disableOnProperty": "valueBox",
"disabled": true, "disabled": false,
"visible": true "visible": true
}, },
{ {
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

22
application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

22
application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

22
application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg

@ -35,7 +35,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -314,7 +314,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -370,6 +370,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -392,16 +400,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

26
application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -365,7 +365,17 @@
"value": false, "value": false,
"label": "Absolute" "label": "Absolute"
} }
] ],
"disabled": false,
"visible": true
},
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
}, },
{ {
"id": "transparent", "id": "transparent",
@ -389,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

20
application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "night-rate", "tag": "night-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -48,7 +48,7 @@
}, },
{ {
"tag": "off-peak-rate", "tag": "off-peak-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -58,12 +58,12 @@
}, },
{ {
"tag": "peak-rate", "tag": "peak-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -480,6 +480,7 @@
"group": "{i18n:scada.symbol.off-peak-rate}", "group": "{i18n:scada.symbol.off-peak-rate}",
"type": "text", "type": "text",
"default": "T1", "default": "T1",
"disableOnProperty": "showOffPeakLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -496,6 +497,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showOffPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -505,6 +507,7 @@
"group": "{i18n:scada.symbol.off-peak-rate}", "group": "{i18n:scada.symbol.off-peak-rate}",
"type": "color", "type": "color",
"default": "#000000", "default": "#000000",
"disableOnProperty": "showOffPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -556,6 +559,7 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "text", "type": "text",
"default": "T2", "default": "T2",
"disableOnProperty": "showNightLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -572,6 +576,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showNightLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -581,6 +586,7 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showNightLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -632,6 +638,7 @@
"group": "{i18n:scada.symbol.peak-rate}", "group": "{i18n:scada.symbol.peak-rate}",
"type": "text", "type": "text",
"default": "T3", "default": "T3",
"disableOnProperty": "showPeakLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": false,
"visible": true "visible": true
@ -648,6 +655,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -657,6 +665,7 @@
"group": "{i18n:scada.symbol.peak-rate}", "group": "{i18n:scada.symbol.peak-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showPeakLabel",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -706,6 +715,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "kWh", "default": "kWh",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -720,6 +730,7 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -728,6 +739,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disableOnProperty": "showUnits",
"disabled": false, "disabled": false,
"visible": true "visible": true
} }

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

7
application/src/main/data/json/system/scada_symbols/top-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -464,8 +464,7 @@
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "supportsUnitConversion": true
"visible": true
}, },
{ {
"id": "valueDecimals", "id": "valueDecimals",

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

31
application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "day-rate", "tag": "day-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.dayValueFont, ctx.properties.dayValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.dayRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.dayValueFont, ctx.properties.dayValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.dayRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
@ -48,12 +48,12 @@
}, },
{ {
"tag": "night-rate", "tag": "night-rate",
"stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));",
"actions": null "actions": null
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -427,8 +427,9 @@
"group": "{i18n:scada.symbol.day-rate}", "group": "{i18n:scada.symbol.day-rate}",
"type": "text", "type": "text",
"default": "T1", "default": "T1",
"disableOnProperty": "showDayLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": true,
"visible": true "visible": true
}, },
{ {
@ -443,7 +444,8 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disabled": false, "disableOnProperty": "showDayLabel",
"disabled": true,
"visible": true "visible": true
}, },
{ {
@ -452,7 +454,8 @@
"group": "{i18n:scada.symbol.day-rate}", "group": "{i18n:scada.symbol.day-rate}",
"type": "color", "type": "color",
"default": "#000000", "default": "#000000",
"disabled": false, "disableOnProperty": "showDayLabel",
"disabled": true,
"visible": true "visible": true
}, },
{ {
@ -503,8 +506,9 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "text", "type": "text",
"default": "T2", "default": "T2",
"disableOnProperty": "showNightLabel",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"disabled": false, "disabled": true,
"visible": true "visible": true
}, },
{ {
@ -519,7 +523,8 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disabled": false, "disableOnProperty": "showNightLabel",
"disabled": true,
"visible": true "visible": true
}, },
{ {
@ -528,7 +533,8 @@
"group": "{i18n:scada.symbol.night-rate}", "group": "{i18n:scada.symbol.night-rate}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disabled": false, "disableOnProperty": "showNightLabel",
"disabled": true,
"visible": true "visible": true
}, },
{ {
@ -577,6 +583,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "kWh", "default": "kWh",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -591,7 +598,8 @@
"weight": "normal", "weight": "normal",
"style": "normal" "style": "normal"
}, },
"disabled": false, "disableOnProperty": "showUnits",
"disabled": true,
"visible": true "visible": true
}, },
{ {
@ -599,7 +607,8 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "color", "type": "color",
"default": "#000", "default": "#000",
"disabled": false, "disableOnProperty": "showUnits",
"disabled": true,
"visible": true "visible": true
} }
] ]

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

5
application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg

@ -57,7 +57,7 @@
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -66,7 +66,7 @@
}, },
{ {
"tag": "valueUnits", "tag": "valueUnits",
"stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n",
"actions": { "actions": {
"click": { "click": {
"actionFunction": "ctx.api.callAction(event, 'displayClick');" "actionFunction": "ctx.api.callAction(event, 'displayClick');"
@ -464,6 +464,7 @@
"type": "units", "type": "units",
"default": "m³/hr", "default": "m³/hr",
"fieldClass": "medium-width", "fieldClass": "medium-width",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

23
application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg

@ -34,7 +34,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -313,7 +313,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,17 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"required": false,
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

30
application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg

@ -38,7 +38,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -346,6 +346,24 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": false,
"supportsUnitConversion": true,
"disabled": false,
"visible": true
},
{ {
"id": "majorIntervals", "id": "majorIntervals",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",
@ -359,16 +377,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

22
application/src/main/data/json/system/scada_symbols/vertical-tank.svg

@ -33,7 +33,7 @@
}, },
{ {
"tag": "scale", "tag": "scale",
"stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}",
"actions": null "actions": null
}, },
{ {
@ -312,7 +312,7 @@
"type": "units", "type": "units",
"default": "gal", "default": "gal",
"subLabel": "{i18n:scada.symbol.units}", "subLabel": "{i18n:scada.symbol.units}",
"disableOnProperty": "valueBox", "supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
@ -369,6 +369,14 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "enableUnitScale",
"name": "{i18n:scada.symbol.enable-units-scale}",
"type": "switch",
"default": false,
"disabled": false,
"visible": true
},
{ {
"id": "transparent", "id": "transparent",
"name": "{i18n:scada.symbol.transparent-mode}", "name": "{i18n:scada.symbol.transparent-mode}",
@ -391,16 +399,6 @@
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },
{
"id": "majorUnits",
"name": "{i18n:scada.symbol.major-ticks}",
"type": "units",
"subLabel": "{i18n:scada.symbol.units}",
"divider": true,
"disableOnProperty": "scale",
"disabled": false,
"visible": true
},
{ {
"id": "majorFont", "id": "majorFont",
"name": "{i18n:scada.symbol.major-ticks}", "name": "{i18n:scada.symbol.major-ticks}",

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

5
application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg

@ -34,12 +34,12 @@
}, },
{ {
"tag": "units", "tag": "units",
"stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
"tag": "value", "tag": "value",
"stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.voltage, 0, null, 0));\n} else {\n element.hide();\n}", "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.voltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}",
"actions": null "actions": null
}, },
{ {
@ -379,6 +379,7 @@
"name": "{i18n:scada.symbol.units}", "name": "{i18n:scada.symbol.units}",
"type": "units", "type": "units",
"default": "V", "default": "V",
"supportsUnitConversion": true,
"disabled": false, "disabled": false,
"visible": true "visible": true
}, },

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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

@ -199,6 +199,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
@ -420,7 +421,7 @@ public abstract class BaseController {
return handleException(exception, true); return handleException(exception, true);
} }
private ThingsboardException handleException(Exception exception, boolean logException) { private ThingsboardException handleException(Throwable exception, boolean logException) {
if (logException && logControllerErrorStackTrace) { if (logException && logControllerErrorStackTrace) {
try { try {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -431,6 +432,9 @@ public abstract class BaseController {
} }
Throwable cause = exception.getCause(); Throwable cause = exception.getCause();
if (exception instanceof ExecutionException) {
exception = cause;
}
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException

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

@ -244,7 +244,7 @@ public class CalculatedFieldController extends BaseController {
); );
Object[] args = new Object[ctxAndArgNames.size()]; Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLastUpdateTimestamp(arguments)); args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) { for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i)); var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) { if (arg instanceof TbelCfSingleValueArg svArg) {
@ -267,7 +267,7 @@ public class CalculatedFieldController extends BaseController {
return result; return result;
} }
private long getLastUpdateTimestamp(Map<String, TbelCfArg> arguments) { private long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1; long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) { for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) { if (entry instanceof TbelCfSingleValueArg singleValueArg) {

3
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java

@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService; import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import java.util.List; import java.util.List;
@ -35,7 +36,7 @@ public interface CalculatedFieldQueueService extends RuleEngineCalculatedFieldQu
*/ */
void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback<Void> callback); void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback<Void> callback);
void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback); void pushRequestToQueue(AttributesSaveRequest request, AttributesSaveResult result, FutureCallback<Void> callback);
void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback); void pushRequestToQueue(AttributesDeleteRequest request, List<String> result, FutureCallback<Void> callback);

10
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
@ -98,7 +99,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
} }
@Override @Override
public void pushRequestToQueue(AttributesSaveRequest request, List<Long> result, FutureCallback<Void> callback) { public void pushRequestToQueue(AttributesSaveRequest request, AttributesSaveResult result, FutureCallback<Void> callback) {
var tenantId = request.getTenantId(); var tenantId = request.getTenantId();
var entityId = request.getEntityId(); var entityId = request.getEntityId();
var entries = request.getEntries(); var entries = request.getEntries();
@ -209,15 +210,18 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
return ToCalculatedFieldMsg.newBuilder().setTelemetryMsg(telemetryMsg).build(); return ToCalculatedFieldMsg.newBuilder().setTelemetryMsg(telemetryMsg).build();
} }
private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, List<Long> versions) { private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, AttributesSaveResult result) {
ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder();
CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType()); CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType());
telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name())); telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name()));
List<AttributeKvEntry> entries = request.getEntries(); List<AttributeKvEntry> entries = request.getEntries();
List<Long> versions = result.versions();
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder(); AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder();
if (versions != null) { if (versions != null && !versions.isEmpty() && versions.get(i) != null) {
attrProtoBuilder.setVersion(versions.get(i)); attrProtoBuilder.setVersion(versions.get(i));
} }
telemetryMsg.addAttrData(attrProtoBuilder.build()); telemetryMsg.addAttrData(attrProtoBuilder.build());

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

@ -35,7 +35,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected Map<String, ArgumentEntry> arguments; protected Map<String, ArgumentEntry> arguments;
protected boolean sizeExceedsLimit; protected boolean sizeExceedsLimit;
protected long lastUpdateTimestamp = -1; protected long latestTimestamp = -1;
public BaseCalculatedFieldState(List<String> requiredArguments) { public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments; this.requiredArguments = requiredArguments;
@ -110,12 +110,14 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected abstract void validateNewEntry(ArgumentEntry newEntry); protected abstract void validateNewEntry(ArgumentEntry newEntry);
private void updateLastUpdateTimestamp(ArgumentEntry entry) { private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp;
if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) {
this.lastUpdateTimestamp = singleValueArgumentEntry.getTs(); newTs = singleValueArgumentEntry.getTs();
} else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { } else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) {
Map.Entry<Long, Double> lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); Map.Entry<Long, Double> lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry();
this.lastUpdateTimestamp = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis(); newTs = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis();
} }
this.latestTimestamp = Math.max(this.latestTimestamp, newTs);
} }
} }

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

@ -62,7 +62,7 @@ public class CalculatedFieldCtx {
private final List<String> argNames; private final List<String> argNames;
private Output output; private Output output;
private String expression; private String expression;
private boolean preserveMsgTs; private boolean useLatestTs;
private TbelInvokeService tbelInvokeService; private TbelInvokeService tbelInvokeService;
private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
private ThreadLocal<Expression> customExpression; private ThreadLocal<Expression> customExpression;
@ -96,7 +96,7 @@ public class CalculatedFieldCtx {
this.argNames = new ArrayList<>(arguments.keySet()); this.argNames = new ArrayList<>(arguments.keySet());
this.output = configuration.getOutput(); this.output = configuration.getOutput();
this.expression = configuration.getExpression(); this.expression = configuration.getExpression();
this.preserveMsgTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isPreserveMsgTs(); this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs();
this.tbelInvokeService = tbelInvokeService; this.tbelInvokeService = tbelInvokeService;
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);

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

@ -42,7 +42,7 @@ public interface CalculatedFieldState {
Map<String, ArgumentEntry> getArguments(); Map<String, ArgumentEntry> getArguments();
long getLastUpdateTimestamp(); long getLatestTimestamp();
void setRequiredArguments(List<String> requiredArguments); void setRequiredArguments(List<String> requiredArguments);

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

@ -66,7 +66,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
args.add(arg); args.add(arg);
} }
} }
args.set(0, new TbelCfCtx(arguments, getLastUpdateTimestamp())); args.set(0, new TbelCfCtx(arguments, getLatestTimestamp()));
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray());
Output output = ctx.getOutput(); Output output = ctx.getOutput();
return Futures.transform(resultFuture, return Futures.transform(resultFuture,

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

@ -79,7 +79,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
Output output = ctx.getOutput(); Output output = ctx.getOutput();
Object result = formatResult(expressionResult, output.getDecimalsByDefault()); Object result = formatResult(expressionResult, output.getDecimalsByDefault());
JsonNode outputResult = createResultJson(ctx.isPreserveMsgTs(), output.getName(), result); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result);
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult));
} }
@ -94,7 +94,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
return TbUtils.toFixed(expressionResult, decimals); return TbUtils.toFixed(expressionResult, decimals);
} }
private JsonNode createResultJson(boolean preserveMsgTs, String outputName, Object result) { private JsonNode createResultJson(boolean useLatestTs, String outputName, Object result) {
ObjectNode valuesNode = JacksonUtil.newObjectNode(); ObjectNode valuesNode = JacksonUtil.newObjectNode();
if (result instanceof Double doubleValue) { if (result instanceof Double doubleValue) {
valuesNode.put(outputName, doubleValue); valuesNode.put(outputName, doubleValue);
@ -104,10 +104,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
valuesNode.set(outputName, JacksonUtil.valueToTree(result)); valuesNode.set(outputName, JacksonUtil.valueToTree(result));
} }
long lastTimestamp = getLastUpdateTimestamp(); long latestTs = getLatestTimestamp();
if (preserveMsgTs && lastTimestamp != -1) { if (useLatestTs && latestTs != -1) {
ObjectNode resultNode = JacksonUtil.newObjectNode(); ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", lastTimestamp); resultNode.put("ts", latestTs);
resultNode.set("values", valuesNode); resultNode.set("values", valuesNode);
return resultNode; return resultNode;
} else { } else {

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
@ -62,8 +63,6 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -240,10 +239,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
} }
private ListenableFuture<List<Long>> saveProvisionStateAttribute(Device device) { private ListenableFuture<AttributesSaveResult> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, return attributesService.save(
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE,
System.currentTimeMillis()))); new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), System.currentTimeMillis())
);
} }
private DeviceCredentials getDeviceCredentials(Device device) { private DeviceCredentials getDeviceCredentials(Device device) {

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

@ -69,7 +69,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -193,6 +195,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!"); log.info("Edge RPC service initialized!");
} }
@ -262,6 +265,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Override @Override
public void updateEdge(TenantId tenantId, Edge edge) { public void updateEdge(TenantId tenantId, Edge edge) {
if (edge == null) {
log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId);
return;
}
EdgeGrpcSession session = sessions.get(edge.getId()); EdgeGrpcSession session = sessions.get(edge.getId());
if (session != null && session.isConnected()) { if (session != null && session.isConnected()) {
log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
@ -459,11 +466,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception { private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception {
boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE); boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE);
if (!isMigrationProcessed) { if (!isMigrationProcessed) {
log.info("Starting edge event migration for edge [{}]", edgeId.getId());
Boolean eventsExist = session.migrateEdgeEvents().get(); Boolean eventsExist = session.migrateEdgeEvents().get();
if (Boolean.TRUE.equals(eventsExist)) { if (Boolean.TRUE.equals(eventsExist)) {
log.info("Migration still in progress for edge [{}]", edgeId.getId());
sessionNewEvents.put(edgeId, true); sessionNewEvents.put(edgeId, true);
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} else if (Boolean.FALSE.equals(eventsExist)) { } else if (Boolean.FALSE.equals(eventsExist)) {
log.info("Migration completed for edge [{}]", edgeId.getId());
edgeEventsMigrationProcessed.put(edgeId, true); edgeEventsMigrationProcessed.put(edgeId, true);
} }
} }
@ -610,4 +620,27 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId());
}
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
kafkaSession.destroy();
}
}
} catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", e);
}
}
} }

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
@ -42,7 +43,6 @@ import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
@ -292,11 +292,11 @@ public abstract class EdgeGrpcSession implements Closeable {
protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) { protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try { try {
log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink);
processHighPriorityEvents(); processHighPriorityEvents();
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, sessionId, pageData.getData().size()); log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@Override @Override
@ -323,16 +323,16 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}] Failed to send downlink msgs pack", sessionId, t); log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink);
result.set(null); result.set(null);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to fetch edge events", sessionId, e); log.error("[{}] Failed to fetch edge events", edge.getId(), e);
result.setException(e); result.setException(e);
} }
} }
@ -459,9 +459,9 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
} }
log.warn("[{}][{}] {}, attempt: {}", tenantId, sessionId, failureMsg, attempt); log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt);
} }
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, sessionId, copy.size()); log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size());
for (DownlinkMsg downlinkMsg : copy) { for (DownlinkMsg downlinkMsg : copy) {
if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) {
String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " +
@ -483,7 +483,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} else { } else {
String failureMsg = String.format("Failed to deliver messages: %s", copy); String failureMsg = String.format("Failed to deliver messages: %s", copy);
log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
tenantId, sessionId, MAX_DOWNLINK_ATTEMPTS, copy); tenantId, edge.getId(), MAX_DOWNLINK_ATTEMPTS, copy);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
@ -493,7 +493,7 @@ public abstract class EdgeGrpcSession implements Closeable {
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, sessionId, e.getMessage(), e); log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, edge.getId(), e.getMessage(), e);
stopCurrentSendDownlinkMsgsTask(true); stopCurrentSendDownlinkMsgsTask(true);
} }
}; };
@ -540,7 +540,7 @@ public abstract class EdgeGrpcSession implements Closeable {
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, sessionId, msg, e); log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, edge.getId(), msg, e);
} }
} }
@ -555,12 +555,12 @@ public abstract class EdgeGrpcSession implements Closeable {
while ((event = highPriorityQueue.poll()) != null) { while ((event = highPriorityQueue.poll()) != null) {
highPriorityEvents.add(event); highPriorityEvents.add(event);
} }
log.trace("[{}][{}] Sending high priority events {}", tenantId, sessionId, highPriorityEvents.size()); log.trace("[{}][{}] Sending high priority events {}", tenantId, edge.getId(), highPriorityEvents.size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents);
sendDownlinkMsgsPack(downlinkMsgsPack).get(); sendDownlinkMsgsPack(downlinkMsgsPack).get();
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to process high priority events", sessionId, e); log.error("[{}] Failed to process high priority events", edge.getId(), e);
} }
} }
@ -577,16 +577,16 @@ public abstract class EdgeGrpcSession implements Closeable {
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService()); ctx.getEdgeEventService());
log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}",
tenantId, sessionId, previousStartTs, previousStartSeqId); tenantId, edge.getId(), previousStartTs, previousStartSeqId);
Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (newStartTsAndSeqId != null) { if (newStartTsAndSeqId != null) {
ListenableFuture<List<Long>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); ListenableFuture<AttributesSaveResult> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() { Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<Long> list) { public void onSuccess(@Nullable AttributesSaveResult saveResult) {
log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edge.getId(), newStartTsAndSeqId);
boolean newEventsAvailable; boolean newEventsAvailable;
if (fetcher.isSeqIdNewCycleStarted()) { if (fetcher.isSeqIdNewCycleStarted()) {
newEventsAvailable = isNewEdgeEventsAvailable(); newEventsAvailable = isNewEdgeEventsAvailable();
@ -601,28 +601,28 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, sessionId, newStartTsAndSeqId, t); log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edge.getId(), newStartTsAndSeqId, t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, sessionId); log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edge.getId());
result.set(Boolean.FALSE); result.set(Boolean.FALSE);
} }
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to process events", tenantId, sessionId, t); log.error("[{}][{}] Failed to process events", tenantId, edge.getId(), t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
if (isSyncInProgress()) { if (isSyncInProgress()) {
log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, sessionId); log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edge.getId());
result.set(Boolean.TRUE); result.set(Boolean.TRUE);
} else { } else {
log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, sessionId); log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edge.getId());
result.set(null); result.set(null);
} }
} }
@ -632,7 +632,7 @@ public abstract class EdgeGrpcSession implements Closeable {
protected List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) { protected List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
List<DownlinkMsg> result = new ArrayList<>(); List<DownlinkMsg> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) { for (EdgeEvent edgeEvent : edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, sessionId, edgeEvent); log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent);
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
try { try {
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -641,17 +641,16 @@ public abstract class EdgeGrpcSession implements Closeable {
ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
downlinkMsg = convertEntityEventToDownlink(edgeEvent); downlinkMsg = convertEntityEventToDownlink(edgeEvent);
if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) {
log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, sessionId, downlinkMsg.getDownlinkMsgId()); log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, edge.getId(), downlinkMsg.getDownlinkMsgId());
} else { } else {
log.trace("[{}][{}] entity message processed [{}]", tenantId, sessionId, downlinkMsg); log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg);
} }
} }
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction());
default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction());
} }
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, edge.getId(), e);
} }
if (downlinkMsg != null) { if (downlinkMsg != null) {
result.add(downlinkMsg); result.add(downlinkMsg);
@ -723,13 +722,14 @@ public abstract class EdgeGrpcSession implements Closeable {
return startSeqId; return startSeqId;
} }
private ListenableFuture<List<Long>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) { private ListenableFuture<AttributesSaveResult> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
newStartTs = pair.getFirst(); newStartTs = pair.getFirst();
newStartSeqId = pair.getSecond(); newStartSeqId = pair.getSecond();
log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", sessionId, edge.getId(), newStartTs, newStartSeqId); log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", sessionId, edge.getId(), newStartTs, newStartSeqId);
List<AttributeKvEntry> attributes = Arrays.asList( List<AttributeKvEntry> attributes = List.of(
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()),
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis())); new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis())
);
return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, attributes); return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, attributes);
} }
@ -757,19 +757,19 @@ public abstract class EdgeGrpcSession implements Closeable {
private void sendDownlinkMsg(ResponseMsg responseMsg) { private void sendDownlinkMsg(ResponseMsg responseMsg) {
if (isConnected()) { if (isConnected()) {
String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000);
log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, sessionId, responseMsgStr); log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, edge.getId(), responseMsgStr);
downlinkMsgLock.lock(); downlinkMsgLock.lock();
String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr;
try { try {
outputStream.onNext(responseMsg); outputStream.onNext(responseMsg);
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, edge.getId(), downlinkMsgStr, e);
connected = false; connected = false;
sessionCloseListener.accept(edge, sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
downlinkMsgLock.unlock(); downlinkMsgLock.unlock();
} }
log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, sessionId, downlinkMsgStr); log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, edge.getId(), downlinkMsgStr);
} }
} }
@ -909,8 +909,8 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }

90
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -56,6 +57,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
private volatile boolean isHighPriorityProcessing; private volatile boolean isHighPriorityProcessing;
@Getter
private QueueConsumerManager<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer; private QueueConsumerManager<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer;
private ExecutorService consumerExecutor; private ExecutorService consumerExecutor;
@ -72,31 +74,28 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
} }
private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) { private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) {
log.trace("[{}][{}] starting processing edge events", tenantId, sessionId); log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId());
if (isConnected() && !isSyncInProgress() && !isHighPriorityProcessing) { if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
List<EdgeEvent> edgeEvents = new ArrayList<>(); log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " +
for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) { "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration",
EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing);
edgeEvents.add(edgeEvent); return;
} }
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); List<EdgeEvent> edgeEvents = new ArrayList<>();
try { for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) {
boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg());
if (isInterrupted) { edgeEvents.add(edgeEvent);
log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); }
} else { List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents);
consumer.commit(); try {
} boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get();
} catch (Exception e) { if (isInterrupted) {
log.error("[{}] Failed to process all downlink messages", sessionId, e); log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId());
} } else {
} else { consumer.commit();
try {
Thread.sleep(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval());
} catch (InterruptedException interruptedException) {
log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException);
} }
log.trace("[{}][{}] edge is not connected or sync is not completed. Skipping iteration", tenantId, sessionId); } catch (Exception e) {
log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e);
} }
} }
@ -107,18 +106,23 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override @Override
public ListenableFuture<Boolean> processEdgeEvents() { public ListenableFuture<Boolean> processEdgeEvents() {
if (consumer == null) { if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); try {
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder() this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
.name("TB Edge events") this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.msgPackProcessor(this::processMsgs) .name("TB Edge events [" + edge.getId() + "]")
.pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) .msgPackProcessor(this::processMsgs)
.consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval())
.consumerExecutor(consumerExecutor) .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId()))
.threadPrefix("edge-events") .consumerExecutor(consumerExecutor)
.build(); .threadPrefix("edge-events-" + edge.getId())
consumer.subscribe(); .build();
consumer.launch(); consumer.subscribe();
consumer.launch();
} catch (Exception e) {
destroy();
log.warn("[{}][{}] Failed to start edge event consumer", sessionId, edge.getId(), e);
}
} }
return Futures.immediateFuture(Boolean.FALSE); return Futures.immediateFuture(Boolean.FALSE);
} }
@ -132,8 +136,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override @Override
public void destroy() { public void destroy() {
consumer.stop(); try {
consumerExecutor.shutdown(); if (consumer != null) {
consumer.stop();
}
} finally {
consumer = null;
}
try {
if (consumerExecutor != null) {
consumerExecutor.shutdown();
}
} catch (Exception ignored) {}
} }
@Override @Override

9
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
@ -581,9 +582,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L); Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L);
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));
} else { } else {
ListenableFuture<List<Long>> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, ListenableFuture<AttributesSaveResult> saveFuture = attributesService.save(
Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry(new BooleanDataEntry(key, value), System.currentTimeMillis())
, System.currentTimeMillis()))); );
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));
} }
} }
@ -611,7 +612,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
} }
private <S> void addTsCallback(ListenableFuture<S> saveFuture, final FutureCallback<S> callback) { private <S> void addTsCallback(ListenableFuture<S> saveFuture, final FutureCallback<S> callback) {
Futures.addCallback(saveFuture, new FutureCallback<S>() { Futures.addCallback(saveFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable S result) { public void onSuccess(@Nullable S result) {
callback.onSuccess(result); callback.onSuccess(result);

33
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -66,8 +67,8 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationDeduplicationService deduplicationService; private final NotificationDeduplicationService deduplicationService;
private final PartitionService partitionService; private final PartitionService partitionService;
private final RateLimitService rateLimitService; private final RateLimitService rateLimitService;
@Autowired @Lazy @Lazy
private NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
@ -82,14 +83,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
if (enabledRules.isEmpty()) { if (enabledRules.isEmpty()) {
return; return;
} }
if (trigger.deduplicate()) {
enabledRules = new ArrayList<>(enabledRules); List<NotificationRule> rulesToProcess = filterNotificationRules(trigger, enabledRules);
enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule)); for (NotificationRule rule : rulesToProcess) {
}
final List<NotificationRule> rules = enabledRules;
for (NotificationRule rule : rules) {
try { try {
processNotificationRule(rule, trigger); processNotificationRule(rule, trigger, DeduplicationStrategy.ONLY_MATCHING.equals(trigger.getDeduplicationStrategy()));
} catch (Throwable e) { } catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
} }
@ -100,7 +98,20 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
}); });
} }
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger) { private List<NotificationRule> filterNotificationRules(NotificationRuleTrigger trigger, List<NotificationRule> enabledRules) {
List<NotificationRule> rulesToProcess = new ArrayList<>(enabledRules);
rulesToProcess.removeIf(rule -> switch (trigger.getDeduplicationStrategy()) {
case ONLY_MATCHING -> {
boolean matched = matchesFilter(trigger, rule.getTriggerConfig());
yield !matched || deduplicationService.alreadyProcessed(trigger, rule);
}
case ALL -> deduplicationService.alreadyProcessed(trigger, rule);
default -> false;
});
return rulesToProcess;
}
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger, boolean alreadyMatched) {
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig();
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType());
@ -114,7 +125,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return; return;
} }
if (matchesFilter(trigger, triggerConfig)) { if (alreadyMatched || matchesFilter(trigger, triggerConfig)) {
if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) { if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) {
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName()); log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return; return;

7
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java

@ -39,7 +39,12 @@ public class ResourcesShortageTriggerProcessor implements NotificationRuleTrigge
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) {
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build(); return ResourcesShortageNotificationInfo.builder()
.resource(trigger.getResource().name())
.usage(trigger.getUsage())
.serviceId(trigger.getServiceId())
.serviceType(trigger.getServiceType())
.build();
} }
@Override @Override

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

@ -27,11 +27,10 @@ import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -170,35 +169,22 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
@Lazy @Lazy
private TelemetrySubscriptionService tsSubService; private TelemetrySubscriptionService tsSubService;
@Value("${state.defaultInactivityTimeoutInSec}")
@Getter
@Setter
private long defaultInactivityTimeoutInSec;
@Value("#{${state.defaultInactivityTimeoutInSec} * 1000}") @Value("#{${state.defaultInactivityTimeoutInSec} * 1000}")
@Getter
@Setter
private long defaultInactivityTimeoutMs; private long defaultInactivityTimeoutMs;
@Value("${state.defaultStateCheckIntervalInSec}") @Value("${state.defaultStateCheckIntervalInSec}")
@Getter
private int defaultStateCheckIntervalInSec; private int defaultStateCheckIntervalInSec;
@Value("${usage.stats.devices.report_interval:60}") @Value("${usage.stats.devices.report_interval:60}")
@Getter
private int defaultActivityStatsIntervalInSec; private int defaultActivityStatsIntervalInSec;
@Value("${state.persistToTelemetry:false}") @Value("${state.persistToTelemetry:false}")
@Getter
@Setter
private boolean persistToTelemetry; private boolean persistToTelemetry;
@Value("${state.initFetchPackSize:50000}") @Value("${state.initFetchPackSize:50000}")
@Getter
private int initFetchPackSize; private int initFetchPackSize;
@Value("${state.telemetryTtl:0}") @Value("${state.telemetryTtl:0}")
@Getter
private int telemetryTtl; private int telemetryTtl;
private ListeningExecutorService deviceStateExecutor; private ListeningExecutorService deviceStateExecutor;
@ -281,12 +267,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
state.setLastActivityTime(lastReportedActivity); state.setLastActivityTime(lastReportedActivity);
if (!state.isActive()) { if (!state.isActive()) {
state.setActive(true);
if (lastReportedActivity <= state.getLastInactivityAlarmTime()) { if (lastReportedActivity <= state.getLastInactivityAlarmTime()) {
state.setLastInactivityAlarmTime(0); state.setLastInactivityAlarmTime(0);
save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, 0); save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, 0);
} }
onDeviceActivityStatusChange(deviceId, true, stateData); onDeviceActivityStatusChange(true, stateData);
} }
} else { } else {
log.debug("updateActivityState - fetched state IS NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); log.debug("updateActivityState - fetched state IS NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity);
@ -355,7 +340,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return; return;
} }
log.trace("[{}][{}] On device inactivity: processing inactivity event with ts [{}].", tenantId.getId(), deviceId.getId(), lastInactivityTime); log.trace("[{}][{}] On device inactivity: processing inactivity event with ts [{}].", tenantId.getId(), deviceId.getId(), lastInactivityTime);
reportInactivity(lastInactivityTime, deviceId, stateData); reportInactivity(lastInactivityTime, stateData);
} }
@Override @Override
@ -387,7 +372,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(@NonNull Throwable t) {
log.warn("Failed to register device to the state service", t); log.warn("Failed to register device to the state service", t);
callback.onFailure(t); callback.onFailure(t);
} }
@ -539,7 +524,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
} }
void reportActivityStats() { private void reportActivityStats() {
try { try {
Map<TenantId, Pair<AtomicInteger, AtomicInteger>> stats = new HashMap<>(); Map<TenantId, Pair<AtomicInteger, AtomicInteger>> stats = new HashMap<>();
for (DeviceStateData stateData : deviceStates.values()) { for (DeviceStateData stateData : deviceStates.values()) {
@ -574,7 +559,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
&& (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() <= state.getLastActivityTime()) && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() <= state.getLastActivityTime())
&& stateData.getDeviceCreationTime() + state.getInactivityTimeout() <= ts) { && stateData.getDeviceCreationTime() + state.getInactivityTimeout() <= ts) {
if (partitionService.resolve(ServiceType.TB_CORE, stateData.getTenantId(), deviceId).isMyPartition()) { if (partitionService.resolve(ServiceType.TB_CORE, stateData.getTenantId(), deviceId).isMyPartition()) {
reportInactivity(ts, deviceId, stateData); reportInactivity(ts, stateData);
} else { } else {
cleanupEntity(deviceId); cleanupEntity(deviceId);
} }
@ -585,15 +570,25 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
} }
private void reportInactivity(long ts, DeviceId deviceId, DeviceStateData stateData) { private void reportInactivity(long ts, DeviceStateData stateData) {
DeviceState state = stateData.getState(); var tenantId = stateData.getTenantId();
state.setActive(false); var deviceId = stateData.getDeviceId();
state.setLastInactivityAlarmTime(ts);
save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, ts); Futures.addCallback(save(stateData.getTenantId(), deviceId, INACTIVITY_ALARM_TIME, ts), new FutureCallback<>() {
onDeviceActivityStatusChange(deviceId, false, stateData); @Override
public void onSuccess(Void success) {
stateData.getState().setLastInactivityAlarmTime(ts);
onDeviceActivityStatusChange(false, stateData);
}
@Override
public void onFailure(@NonNull Throwable t) {
log.error("[{}][{}] Failed to update device last inactivity alarm time to '{}'. Device state data: {}", tenantId, deviceId, ts, stateData, t);
}
}, deviceStateCallbackExecutor);
} }
boolean isActive(long ts, DeviceState state) { private static boolean isActive(long ts, DeviceState state) {
return ts < state.getLastActivityTime() + state.getInactivityTimeout(); return ts < state.getLastActivityTime() + state.getInactivityTimeout();
} }
@ -616,17 +611,32 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
} }
private void onDeviceActivityStatusChange(DeviceId deviceId, boolean active, DeviceStateData stateData) { private void onDeviceActivityStatusChange(boolean active, DeviceStateData stateData) {
save(stateData.getTenantId(), deviceId, ACTIVITY_STATE, active); var tenantId = stateData.getTenantId();
pushRuleEngineMessage(stateData, active ? TbMsgType.ACTIVITY_EVENT : TbMsgType.INACTIVITY_EVENT); var deviceId = stateData.getDeviceId();
TbMsgMetaData metaData = stateData.getMetaData();
notificationRuleProcessor.process(DeviceActivityTrigger.builder() Futures.addCallback(save(tenantId, deviceId, ACTIVITY_STATE, active), new FutureCallback<>() {
.tenantId(stateData.getTenantId()).customerId(stateData.getCustomerId()) @Override
.deviceId(deviceId).active(active) public void onSuccess(Void success) {
.deviceName(metaData.getValue("deviceName")) stateData.getState().setActive(active);
.deviceType(metaData.getValue("deviceType")) pushRuleEngineMessage(stateData, active ? TbMsgType.ACTIVITY_EVENT : TbMsgType.INACTIVITY_EVENT);
.deviceLabel(metaData.getValue("deviceLabel")) TbMsgMetaData metaData = stateData.getMetaData();
.build()); notificationRuleProcessor.process(DeviceActivityTrigger.builder()
.tenantId(tenantId)
.customerId(stateData.getCustomerId())
.deviceId(deviceId)
.active(active)
.deviceName(metaData.getValue("deviceName"))
.deviceType(metaData.getValue("deviceType"))
.deviceLabel(metaData.getValue("deviceLabel"))
.build());
}
@Override
public void onFailure(@NonNull Throwable t) {
log.error("[{}][{}] Failed to change device activity status to '{}'. Device state data: {}", tenantId, deviceId, active, stateData, t);
}
}, deviceStateCallbackExecutor);
} }
boolean cleanDeviceStateIfBelongsToExternalPartition(TenantId tenantId, final DeviceId deviceId) { boolean cleanDeviceStateIfBelongsToExternalPartition(TenantId tenantId, final DeviceId deviceId) {
@ -634,8 +644,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
boolean cleanup = !partitionedEntities.containsKey(tpi); boolean cleanup = !partitionedEntities.containsKey(tpi);
if (cleanup) { if (cleanup) {
cleanupEntity(deviceId); cleanupEntity(deviceId);
log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}" log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName());
, tenantId, deviceId, tpi.getFullTopicName());
} }
return cleanup; return cleanup;
} }
@ -766,7 +775,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) { private DeviceStateData toDeviceStateData(EntityData ed, DeviceIdInfo deviceIdInfo) {
long lastActivityTime = getEntryValue(ed, getKeyType(), LAST_ACTIVITY_TIME, 0L); long lastActivityTime = getEntryValue(ed, getKeyType(), LAST_ACTIVITY_TIME, 0L);
long inactivityAlarmTime = getEntryValue(ed, getKeyType(), INACTIVITY_ALARM_TIME, 0L); long inactivityAlarmTime = getEntryValue(ed, getKeyType(), INACTIVITY_ALARM_TIME, 0L);
long inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs); long inactivityTimeout = getEntryValue(ed, EntityKeyType.SERVER_ATTRIBUTE, INACTIVITY_TIMEOUT, defaultInactivityTimeoutMs);
@ -849,6 +858,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
private void pushRuleEngineMessage(DeviceStateData stateData, TbMsgType msgType) { private void pushRuleEngineMessage(DeviceStateData stateData, TbMsgType msgType) {
var tenantId = stateData.getTenantId();
var deviceId = stateData.getDeviceId();
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
try { try {
String data; String data;
@ -865,7 +877,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
TbMsg tbMsg = TbMsg.newMsg() TbMsg tbMsg = TbMsg.newMsg()
.type(msgType) .type(msgType)
.originator(stateData.getDeviceId()) .originator(deviceId)
.customerId(stateData.getCustomerId()) .customerId(stateData.getCustomerId())
.copyMetaData(md) .copyMetaData(md)
.dataType(TbMsgDataType.JSON) .dataType(TbMsgDataType.JSON)
@ -873,21 +885,22 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.build(); .build();
clusterService.pushMsgToRuleEngine(stateData.getTenantId(), stateData.getDeviceId(), tbMsg, null); clusterService.pushMsgToRuleEngine(stateData.getTenantId(), stateData.getDeviceId(), tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to push inactivity alarm: {}", stateData.getDeviceId(), state, e); log.warn("[{}][{}] Failed to push '{}' message to the rule engine due to {}. Device state: {}", tenantId, deviceId, msgType, e.getMessage(), state);
} }
} }
private void save(TenantId tenantId, DeviceId deviceId, String key, long value) { private ListenableFuture<Void> save(TenantId tenantId, DeviceId deviceId, String key, long value) {
save(tenantId, deviceId, new LongDataEntry(key, value), getCurrentTimeMillis()); return save(tenantId, deviceId, new LongDataEntry(key, value), getCurrentTimeMillis());
} }
private void save(TenantId tenantId, DeviceId deviceId, String key, boolean value) { private ListenableFuture<Void> save(TenantId tenantId, DeviceId deviceId, String key, boolean value) {
save(tenantId, deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis()); return save(tenantId, deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis());
} }
private void save(TenantId tenantId, DeviceId deviceId, KvEntry kvEntry, long ts) { private ListenableFuture<Void> save(TenantId tenantId, DeviceId deviceId, KvEntry kvEntry, long ts) {
ListenableFuture<?> future;
if (persistToTelemetry) { if (persistToTelemetry) {
tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() future = tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(deviceId) .entityId(deviceId)
.entry(new BasicTsKvEntry(ts, kvEntry)) .entry(new BasicTsKvEntry(ts, kvEntry))
@ -895,7 +908,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.callback(new TelemetrySaveCallback<>(deviceId, kvEntry)) .callback(new TelemetrySaveCallback<>(deviceId, kvEntry))
.build()); .build());
} else { } else {
tsSubService.saveAttributes(AttributesSaveRequest.builder() future = tsSubService.saveAttributesInternal(AttributesSaveRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(deviceId) .entityId(deviceId)
.scope(AttributeScope.SERVER_SCOPE) .scope(AttributeScope.SERVER_SCOPE)
@ -903,20 +916,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.callback(new TelemetrySaveCallback<>(deviceId, kvEntry)) .callback(new TelemetrySaveCallback<>(deviceId, kvEntry))
.build()); .build());
} }
return Futures.transform(future, __ -> null, MoreExecutors.directExecutor());
} }
long getCurrentTimeMillis() { long getCurrentTimeMillis() {
return System.currentTimeMillis(); return System.currentTimeMillis();
} }
private static class TelemetrySaveCallback<T> implements FutureCallback<T> { private record TelemetrySaveCallback<T>(DeviceId deviceId, KvEntry kvEntry) implements FutureCallback<T> {
private final DeviceId deviceId;
private final KvEntry kvEntry;
TelemetrySaveCallback(DeviceId deviceId, KvEntry kvEntry) {
this.deviceId = deviceId;
this.kvEntry = kvEntry;
}
@Override @Override
public void onSuccess(@Nullable T result) { public void onSuccess(@Nullable T result) {
@ -924,9 +931,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(@NonNull Throwable t) {
log.warn("[{}] Failed to update entry {}", deviceId, kvEntry, t); log.warn("[{}] Failed to update entry {}", deviceId, kvEntry, t);
} }
} }
} }

25
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -185,9 +185,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
clusterSystemData.forEach(data -> { clusterSystemData.forEach(data -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build()); Arrays.stream(Resource.values()).forEach(resource -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build()); .resource(resource)
.serviceId(data.getServiceId())
.serviceType(data.getServiceType())
.usage(extractResourceUsage(data, resource))
.build());
});
}); });
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
@ -200,17 +205,17 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
getCpuUsage().ifPresent(v -> { getCpuUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getMemoryUsage().ifPresent(v -> { getMemoryUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getDiscSpaceUsage().ifPresent(v -> { getDiscSpaceUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v)))); getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v))));
@ -258,6 +263,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
return infoData; return infoData;
} }
private Long extractResourceUsage(SystemInfoData info, Resource resource) {
return switch (resource) {
case CPU -> info.getCpuUsage();
case RAM -> info.getMemoryUsage();
case STORAGE -> info.getDiscUsage();
};
}
@PreDestroy @PreDestroy
private void destroy() { private void destroy() {
if (scheduler != null) { if (scheduler != null) {

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

@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -62,7 +63,6 @@ import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -191,17 +191,16 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
} }
@Override @Override
public void saveAttributesInternal(AttributesSaveRequest request) { public ListenableFuture<AttributesSaveResult> saveAttributesInternal(AttributesSaveRequest request) {
log.trace("Executing saveInternal [{}]", request);
TenantId tenantId = request.getTenantId(); TenantId tenantId = request.getTenantId();
EntityId entityId = request.getEntityId(); EntityId entityId = request.getEntityId();
AttributesSaveRequest.Strategy strategy = request.getStrategy(); AttributesSaveRequest.Strategy strategy = request.getStrategy();
ListenableFuture<List<Long>> resultFuture; ListenableFuture<AttributesSaveResult> resultFuture;
if (strategy.saveAttributes()) { if (strategy.saveAttributes()) {
resultFuture = attrService.save(tenantId, entityId, request.getScope(), request.getEntries()); resultFuture = attrService.save(tenantId, entityId, request.getScope(), request.getEntries());
} else { } else {
resultFuture = Futures.immediateFuture(Collections.emptyList()); resultFuture = Futures.immediateFuture(AttributesSaveResult.EMPTY);
} }
addMainCallback(resultFuture, result -> { addMainCallback(resultFuture, result -> {
@ -229,6 +228,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
if (strategy.sendWsUpdate()) { if (strategy.sendWsUpdate()) {
addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries())); addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries()));
} }
return resultFuture;
} }
private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) { private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) {

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

@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
/** /**
@ -30,7 +31,7 @@ public interface InternalTelemetryService extends RuleEngineTelemetryService {
ListenableFuture<TimeseriesSaveResult> saveTimeseriesInternal(TimeseriesSaveRequest request); ListenableFuture<TimeseriesSaveResult> saveTimeseriesInternal(TimeseriesSaveRequest request);
void saveAttributesInternal(AttributesSaveRequest request); ListenableFuture<AttributesSaveResult> saveAttributesInternal(AttributesSaveRequest request);
void deleteTimeseriesInternal(TimeseriesDeleteRequest request); void deleteTimeseriesInternal(TimeseriesDeleteRequest request);

4
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -36,6 +36,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -52,7 +53,6 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.util.TenantRateLimitException;
import org.thingsboard.server.exception.UnauthorizedException; import org.thingsboard.server.exception.UnauthorizedException;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -742,7 +742,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override @Override
public void onFailure(Throwable e) { public void onFailure(Throwable e) {
if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) { if (e instanceof RateLimitExceededException || e.getCause() instanceof RateLimitExceededException) {
log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd); log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd);
} else { } else {
log.info(FAILED_TO_FETCH_DATA, e); log.info(FAILED_TO_FETCH_DATA, e);

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

@ -899,6 +899,8 @@ state:
# Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data. # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data.
# 0 means time-to-live mechanism is disabled. # 0 means time-to-live mechanism is disabled.
telemetryTtl: "${STATE_TELEMETRY_TTL:0}" telemetryTtl: "${STATE_TELEMETRY_TTL:0}"
# Number of device records to fetch per batch when initializing device activity states
initFetchPackSize: "${TB_DEVICE_STATE_INIT_FETCH_PACK_SIZE:50000}"
# Configuration properties for rule nodes related to device activity state # Configuration properties for rule nodes related to device activity state
rule: rule:
node: node:

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

@ -464,7 +464,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
} }
@Test @Test
public void testSimpleCalculatedFieldWhenPreserveMsgTsIsTrue() throws Exception { public void testSimpleCalculatedFieldWhenUseLatestTsIsTrue() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L; long ts = System.currentTimeMillis() - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)));
@ -489,7 +489,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
output.setType(OutputType.TIME_SERIES); output.setType(OutputType.TIME_SERIES);
config.setOutput(output); config.setOutput(output);
config.setPreserveMsgTs(true); config.setUseLatestTs(true);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
@ -506,7 +506,69 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
} }
@Test @Test
public void testScriptCalculatedFieldWhenUsedMsgTsInScript() throws Exception { public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndTelemetryBeforeLatest() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis();
long tsA = ts - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA)));
long tsB = ts - 300L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB)));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("a + b");
calculatedField.setDebugSettings(DebugSettings.all());
calculatedField.setConfigurationVersion(1);
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument1 = new Argument();
ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
argument1.setRefEntityKey(refEntityKey1);
Argument argument2 = new Argument();
ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null);
argument2.setRefEntityKey(refEntityKey2);
config.setArguments(Map.of("a", argument1, "b", argument2));
config.setExpression("a + b");
Output output = new Output();
output.setName("c");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
config.setUseLatestTs(true);
calculatedField.setConfiguration(config);
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 c = getLatestTelemetry(testDevice.getId(), "c");
assertThat(c).isNotNull();
assertThat(c.get("c").get(0).get("ts").asText()).isEqualTo(Long.toString(tsB));
assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("6.0");
});
long tsABeforeTsB = tsB - 300L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":10}}", tsABeforeTsB)));
await().alias("update telemetry with ts less than latest -> save result with latest ts").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode c = getLatestTelemetry(testDevice.getId(), "c");
assertThat(c).isNotNull();
assertThat(c.get("c").get(0).get("ts").asText()).isEqualTo(Long.toString(tsB));// also tsB, since this is the latest timestamp
assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("11.0");
});
}
@Test
public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception {
Device testDevice = createDevice("Test device", "1234567890"); Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L; long ts = System.currentTimeMillis() - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)));
@ -524,7 +586,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey); argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("T", argument)); config.setArguments(Map.of("T", argument));
config.setExpression("return {\"ts\": ctx.msgTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};");
Output output = new Output(); Output output = new Output();
output.setType(OutputType.TIME_SERIES); output.setType(OutputType.TIME_SERIES);

39
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -17,8 +17,15 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -73,4 +80,34 @@ public class SingleValueArgumentEntryTest {
void testUpdateEntryWhenValueWasNotChanged() { void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
} }
}
@Test
void testToTbelCfArgWhenJsonIsObject() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "{\"test\": 10}"), 370L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(Map.class);
Map<String, Integer> expectedMap = Map.of("test", 10);
assertThat(singleValueArg.getValue()).isEqualTo(expectedMap);
}
@Test
void testToTbelCfArgWhenJsonIsArray() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "[{\"test\": 10}, {\"test2\": 20}]"), 371L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(List.class);
List<Map<String, Integer>> expectedList = new ArrayList<>();
expectedList.add(Map.of("test", 10));
expectedList.add(Map.of("test2", 20));
assertThat(singleValueArg.getValue()).isEqualTo(expectedList);
}
}

25
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
@ -395,7 +396,7 @@ public class EntityServiceTest extends AbstractControllerTest {
List<Long> highTemperatures = new ArrayList<>(); List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -545,7 +546,7 @@ public class EntityServiceTest extends AbstractControllerTest {
List<Long> highTemperatures = new ArrayList<>(); List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -599,7 +600,7 @@ public class EntityServiceTest extends AbstractControllerTest {
List<Long> highConsumptions = new ArrayList<>(); List<Long> highConsumptions = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < assets.size(); i++) { for (int i = 0; i < assets.size(); i++) {
Asset asset = assets.get(i); Asset asset = assets.get(i);
attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE)); attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE));
@ -1506,7 +1507,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
} }
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
for (AttributeScope currentScope : AttributeScope.values()) { for (AttributeScope currentScope : AttributeScope.values()) {
@ -1578,7 +1579,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
} }
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -1808,7 +1809,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
} }
List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<AttributesSaveResult>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE));
@ -2269,16 +2270,16 @@ public class EntityServiceTest extends AbstractControllerTest {
return filter; return filter;
} }
private ListenableFuture<List<Long>> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { private ListenableFuture<AttributesSaveResult> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) {
KvEntry attrValue = new LongDataEntry(key, value); KvEntry attrValue = new LongDataEntry(key, value);
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); return attributesService.save(tenantId, entityId, scope, List.of(attr));
} }
private ListenableFuture<List<Long>> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { private ListenableFuture<AttributesSaveResult> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) {
KvEntry attrValue = new StringDataEntry(key, value); KvEntry attrValue = new StringDataEntry(key, value);
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); return attributesService.save(tenantId, entityId, scope, List.of(attr));
} }
private ListenableFuture<TimeseriesSaveResult> saveTimeseries(EntityId entityId, String key, Double value) { private ListenableFuture<TimeseriesSaveResult> saveTimeseries(EntityId entityId, String key, Double value) {
@ -2294,8 +2295,8 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
protected void createMultiRootHierarchy(List<Asset> buildings, List<Asset> apartments, protected void createMultiRootHierarchy(List<Asset> buildings, List<Asset> apartments,
Map<String, Map<UUID, String>> entityNameByTypeMap, Map<String, Map<UUID, String>> entityNameByTypeMap,
Map<UUID, UUID> childParentRelationMap) throws InterruptedException { Map<UUID, UUID> childParentRelationMap) throws InterruptedException {
for (int k = 0; k < 3; k++) { for (int k = 0; k < 3; k++) {
Asset building = new Asset(); Asset building = new Asset();
building.setTenantId(tenantId); building.setTenantId(tenantId);

6
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -89,7 +89,7 @@ public class JobManagerTest extends AbstractControllerTest {
@Test @Test
public void testSubmitJob_allTasksSuccessful() { public void testSubmitJob_allTasksSuccessful() {
int tasksCount = 5; int tasksCount = 7;
JobId jobId = submitJob(DummyJobConfiguration.builder() JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount) .successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000) .taskProcessingTimeMs(1000)
@ -154,10 +154,10 @@ public class JobManagerTest extends AbstractControllerTest {
@Test @Test
public void testCancelJob_whileRunning() throws Exception { public void testCancelJob_whileRunning() throws Exception {
int tasksCount = 100; int tasksCount = 200;
JobId jobId = submitJob(DummyJobConfiguration.builder() JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount) .successfulTasksCount(tasksCount)
.taskProcessingTimeMs(100) .taskProcessingTimeMs(50)
.build()).getId(); .build()).getId();
Thread.sleep(500); Thread.sleep(500);

40
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -825,6 +825,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder() notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM) .resource(Resource.RAM)
.usage(15L) .usage(15L)
.serviceType("serviceType")
.serviceId("serviceId")
.build()); .build());
TimeUnit.MILLISECONDS.sleep(300); TimeUnit.MILLISECONDS.sleep(300);
} }
@ -837,10 +839,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder() notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM) .resource(Resource.RAM)
.usage(5L) .usage(5L)
.serviceType("serviceType")
.serviceId("serviceId")
.build()); .build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne()); await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
} }
@Test
public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(1f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
TimeUnit.SECONDS.sleep(5);
assertThat(getMyNotifications(false, 100)).size().isZero();
loginSysAdmin();
triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
rule.setTriggerConfig(triggerConfig);
saveNotificationRule(rule);
loginTenantAdmin();
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
}
@Test @Test
public void testNotificationRuleDisabling() throws Exception { public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

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

@ -16,6 +16,9 @@
package org.thingsboard.server.service.state; package org.thingsboard.server.service.state;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -31,13 +34,13 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
@ -50,20 +53,22 @@ import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.usagestats.DefaultTbApiUsageReportClient; import org.thingsboard.server.queue.usagestats.DefaultTbApiUsageReportClient;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -77,8 +82,8 @@ import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -90,7 +95,10 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class DefaultDeviceStateServiceTest { class DefaultDeviceStateServiceTest {
ListeningExecutorService deviceStateExecutor;
ListeningExecutorService deviceStateCallbackExecutor;
@Mock @Mock
DeviceService deviceService; DeviceService deviceService;
@ -113,25 +121,48 @@ public class DefaultDeviceStateServiceTest {
@Mock @Mock
DefaultTbApiUsageReportClient defaultTbApiUsageReportClient; DefaultTbApiUsageReportClient defaultTbApiUsageReportClient;
TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); long defaultInactivityTimeoutMs = Duration.ofMinutes(10L).toMillis();
DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112");
TopicPartitionInfo tpi; TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"));
DeviceId deviceId = DeviceId.fromString("c209f718-42e5-11f0-9fe2-0242ac120002");
TopicPartitionInfo tpi = TopicPartitionInfo.builder()
.topic("tb_core")
.partition(0)
.myPartition(true)
.build();
DefaultDeviceStateService service; DefaultDeviceStateService service;
@BeforeEach @BeforeEach
public void setUp() { void setUp() {
service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, defaultTbApiUsageReportClient, notificationRuleProcessor)); service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, defaultTbApiUsageReportClient, notificationRuleProcessor));
ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService); ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService);
ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", defaultInactivityTimeoutMs);
ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60); ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60);
ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60); ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60);
ReflectionTestUtils.setField(service, "initFetchPackSize", 10); ReflectionTestUtils.setField(service, "initFetchPackSize", 50000);
deviceStateExecutor = MoreExecutors.newDirectExecutorService();
ReflectionTestUtils.setField(service, "deviceStateExecutor", deviceStateExecutor);
deviceStateCallbackExecutor = MoreExecutors.newDirectExecutorService();
ReflectionTestUtils.setField(service, "deviceStateCallbackExecutor", deviceStateCallbackExecutor);
lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi);
ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedEntities = new ConcurrentHashMap<>();
partitionedEntities.put(tpi, new HashSet<>());
ReflectionTestUtils.setField(service, "partitionedEntities", partitionedEntities);
}
tpi = TopicPartitionInfo.builder().myPartition(true).build(); @AfterEach
void cleanup() {
deviceStateExecutor.shutdownNow();
deviceStateCallbackExecutor.shutdownNow();
} }
@Test @Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() { void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() {
// GIVEN // GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -149,7 +180,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1}) @ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) { void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) {
// GIVEN // GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -166,7 +197,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideOutdatedTimestamps") @MethodSource("provideOutdatedTimestamps")
public void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) { void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) {
// GIVEN // GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -188,7 +219,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() { void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() {
// GIVEN // GIVEN
var deviceStateData = DeviceStateData.builder() var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -202,11 +233,13 @@ public class DefaultDeviceStateServiceTest {
service.deviceStates.put(deviceId, deviceStateData); service.deviceStates.put(deviceId, deviceStateData);
long lastConnectTime = System.currentTimeMillis(); long lastConnectTime = System.currentTimeMillis();
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.onDeviceConnect(tenantId, deviceId, lastConnectTime); service.onDeviceConnect(tenantId, deviceId, lastConnectTime);
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) && request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) &&
@ -221,7 +254,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() { void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() {
// GIVEN // GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -238,7 +271,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1}) @ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) { void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) {
// GIVEN // GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -254,7 +287,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideOutdatedTimestamps") @MethodSource("provideOutdatedTimestamps")
public void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) { void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) {
// GIVEN // GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -275,7 +308,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() { void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() {
// GIVEN // GIVEN
var deviceStateData = DeviceStateData.builder() var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -289,11 +322,13 @@ public class DefaultDeviceStateServiceTest {
service.deviceStates.put(deviceId, deviceStateData); service.deviceStates.put(deviceId, deviceStateData);
long lastDisconnectTime = System.currentTimeMillis(); long lastDisconnectTime = System.currentTimeMillis();
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.onDeviceDisconnect(tenantId, deviceId, lastDisconnectTime); service.onDeviceDisconnect(tenantId, deviceId, lastDisconnectTime);
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) && request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) &&
@ -308,7 +343,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() { void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() {
// GIVEN // GIVEN
doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -325,7 +360,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(longs = {Long.MIN_VALUE, -100, -1}) @ValueSource(longs = {Long.MIN_VALUE, -100, -1})
public void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) { void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) {
// GIVEN // GIVEN
doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId);
@ -341,7 +376,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideOutdatedTimestamps") @MethodSource("provideOutdatedTimestamps")
public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(
long outdatedLastInactivityTime, long currentLastInactivityTime long outdatedLastInactivityTime, long currentLastInactivityTime
) { ) {
// GIVEN // GIVEN
@ -365,7 +400,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideOutdatedTimestamps") @MethodSource("provideOutdatedTimestamps")
public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(
long outdatedLastInactivityTime, long currentLastActivityTime long outdatedLastInactivityTime, long currentLastActivityTime
) { ) {
// GIVEN // GIVEN
@ -398,7 +433,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() {
// GIVEN // GIVEN
var deviceStateData = DeviceStateData.builder() var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -412,17 +447,19 @@ public class DefaultDeviceStateServiceTest {
service.deviceStates.put(deviceId, deviceStateData); service.deviceStates.put(deviceId, deviceStateData);
long lastInactivityTime = System.currentTimeMillis(); long lastInactivityTime = System.currentTimeMillis();
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime); service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime);
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) &&
request.getEntries().get(0).getValue().equals(lastInactivityTime) request.getEntries().get(0).getValue().equals(lastInactivityTime)
)); ));
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
@ -445,7 +482,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() { void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() {
// GIVEN // GIVEN
var deviceStateData = DeviceStateData.builder() var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -456,16 +493,18 @@ public class DefaultDeviceStateServiceTest {
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi);
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData);
// THEN // THEN
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME)
)); ));
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
@ -488,7 +527,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() {
service.deviceStates.put(deviceId, deviceStateDataMock); service.deviceStates.put(deviceId, deviceStateDataMock);
DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId);
assertThat(deviceStateData).isEqualTo(deviceStateDataMock); assertThat(deviceStateData).isEqualTo(deviceStateDataMock);
@ -496,7 +535,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() {
service.deviceStates.clear(); service.deviceStates.clear();
willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId);
@ -504,172 +543,18 @@ public class DefaultDeviceStateServiceTest {
verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId);
} }
private void initStateService(long timeout) throws InterruptedException { @MethodSource
service.stop(); @ParameterizedTest
reset(service, telemetrySubscriptionService); void testOnDeviceInactivityTimeoutUpdate(boolean initialActivityStatus, long newInactivityTimeout, boolean expectedActivityStatus) {
service.setDefaultInactivityTimeoutMs(timeout); // GIVEN
service.init(); doReturn(200L).when(service).getCurrentTimeMillis();
when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi);
when(entityQueryRepository.findEntityDataByQueryInternal(any())).thenReturn(new PageData<>());
var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId());
when(deviceService.findDeviceIdInfos(any()))
.thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false));
PartitionChangeEvent event = new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(
new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi)
), Collections.emptyMap());
service.onApplicationEvent(event);
Thread.sleep(100);
}
@Test
public void increaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(true);
Thread.sleep(increase);
service.checkStates();
activityVerify(false);
reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout + 5);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseInactivityForActiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE)
));
Thread.sleep(defaultTimeout + increase);
service.checkStates();
activityVerify(false);
reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseSmallInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
reset(telemetrySubscriptionService);
long newTimeout = 1;
Thread.sleep(newTimeout);
verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE)
));
}
@Test var deviceState = DeviceState.builder()
public void decreaseInactivityForActiveDeviceTest() throws Exception { .active(initialActivityStatus)
final long defaultTimeout = 1000; .lastActivityTime(100L)
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build(); .build();
service.deviceStates.put(deviceId, deviceStateData); var deviceStateData = DeviceStateData.builder()
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
long newTimeout = 1;
Thread.sleep(newTimeout);
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(false);
reset(telemetrySubscriptionService);
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout);
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void decreaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
.deviceId(deviceId) .deviceId(deviceId)
.state(deviceState) .state(deviceState)
@ -679,31 +564,44 @@ public class DefaultDeviceStateServiceTest {
service.deviceStates.put(deviceId, deviceStateData); service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId); service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); mockSuccessfulSaveAttributes();
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
reset(telemetrySubscriptionService);
long newTimeout = 1; // WHEN
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newInactivityTimeout);
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); // THEN
verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request -> long expectedInactivityTimeout = newInactivityTimeout != 0 ? newInactivityTimeout : defaultInactivityTimeoutMs;
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) assertThat(deviceState.getInactivityTimeout()).isEqualTo(expectedInactivityTimeout);
));
assertThat(deviceState.isActive()).isEqualTo(expectedActivityStatus);
if (initialActivityStatus != expectedActivityStatus) {
then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> {
AttributeKvEntry entry = request.getEntries().get(0);
return request.getEntityId().equals(deviceId) && entry.getKey().equals(ACTIVITY_STATE) && entry.getValue().equals(expectedActivityStatus);
}));
}
} }
private void activityVerify(boolean isActive) { // to simplify test, these arguments assume that the current time is 200 and the last activity time is 100
verify(telemetrySubscriptionService).saveAttributes(argThat(request -> private static Stream<Arguments> testOnDeviceInactivityTimeoutUpdate() {
request.getEntityId().equals(deviceId) && return Stream.of(
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && Arguments.of(true, 1L, false),
request.getEntries().get(0).getValue().equals(isActive) Arguments.of(true, 50L, false),
)); Arguments.of(true, 99L, false),
Arguments.of(true, 100L, false),
Arguments.of(true, 101L, true),
Arguments.of(true, 0L, true), // should use default inactivity timeout of 10 minutes
Arguments.of(false, 1L, false),
Arguments.of(false, 50L, false),
Arguments.of(false, 99L, false),
Arguments.of(false, 100L, false),
Arguments.of(false, 101L, true),
Arguments.of(false, 0L, true) // should use default inactivity timeout of 10 minutes
);
} }
@Test @Test
public void givenStateDataIsNull_whenUpdateActivityState_thenShouldCleanupDevice() { void givenStateDataIsNull_whenUpdateActivityState_thenShouldCleanupDevice() {
// GIVEN // GIVEN
service.deviceStates.put(deviceId, deviceStateDataMock); service.deviceStates.put(deviceId, deviceStateDataMock);
@ -719,7 +617,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideParametersForUpdateActivityState") @MethodSource("provideParametersForUpdateActivityState")
public void givenTestParameters_whenUpdateActivityState_thenShouldBeInTheExpectedStateAndPerformExpectedActions( void givenTestParameters_whenUpdateActivityState_thenShouldBeInTheExpectedStateAndPerformExpectedActions(
boolean activityState, long previousActivityTime, long lastReportedActivity, long inactivityAlarmTime, boolean activityState, long previousActivityTime, long lastReportedActivity, long inactivityAlarmTime,
long expectedInactivityAlarmTime, boolean shouldSetInactivityAlarmTimeToZero, long expectedInactivityAlarmTime, boolean shouldSetInactivityAlarmTimeToZero,
boolean shouldUpdateActivityStateToActive boolean shouldUpdateActivityStateToActive
@ -739,13 +637,15 @@ public class DefaultDeviceStateServiceTest {
.metaData(new TbMsgMetaData()) .metaData(new TbMsgMetaData())
.build(); .build();
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.updateActivityState(deviceId, deviceStateData, lastReportedActivity); service.updateActivityState(deviceId, deviceStateData, lastReportedActivity);
// THEN // THEN
assertThat(deviceState.isActive()).isEqualTo(true); assertThat(deviceState.isActive()).isEqualTo(true);
assertThat(deviceState.getLastActivityTime()).isEqualTo(lastReportedActivity); assertThat(deviceState.getLastActivityTime()).isEqualTo(lastReportedActivity);
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntityId().equals(deviceId) &&
request.getEntries().get(0).getKey().equals(LAST_ACTIVITY_TIME) && request.getEntries().get(0).getKey().equals(LAST_ACTIVITY_TIME) &&
request.getEntries().get(0).getValue().equals(lastReportedActivity) request.getEntries().get(0).getValue().equals(lastReportedActivity)
@ -753,7 +653,7 @@ public class DefaultDeviceStateServiceTest {
assertThat(deviceState.getLastInactivityAlarmTime()).isEqualTo(expectedInactivityAlarmTime); assertThat(deviceState.getLastInactivityAlarmTime()).isEqualTo(expectedInactivityAlarmTime);
if (shouldSetInactivityAlarmTimeToZero) { if (shouldSetInactivityAlarmTimeToZero) {
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntityId().equals(deviceId) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) &&
request.getEntries().get(0).getValue().equals(0L) request.getEntries().get(0).getValue().equals(0L)
@ -761,7 +661,7 @@ public class DefaultDeviceStateServiceTest {
} }
if (shouldUpdateActivityStateToActive) { if (shouldUpdateActivityStateToActive) {
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntityId().equals(deviceId) &&
request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(true) request.getEntries().get(0).getValue().equals(true)
@ -809,59 +709,8 @@ public class DefaultDeviceStateServiceTest {
); );
} }
@ParameterizedTest
@MethodSource("provideParametersForDecreaseInactivityTimeout")
public void givenTestParameters_whenOnDeviceInactivityTimeout_thenShouldBeInTheExpectedStateAndPerformExpectedActions(
boolean activityState, long newInactivityTimeout, long timeIncrement, boolean expectedActivityState
) throws Exception {
// GIVEN
long defaultInactivityTimeout = 10000;
initStateService(defaultInactivityTimeout);
var currentTime = new AtomicLong(System.currentTimeMillis());
DeviceState deviceState = DeviceState.builder()
.active(activityState)
.lastActivityTime(currentTime.get())
.inactivityTimeout(defaultInactivityTimeout)
.build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
given(service.getCurrentTimeMillis()).willReturn(currentTime.addAndGet(timeIncrement));
// WHEN
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newInactivityTimeout);
// THEN
assertThat(deviceState.getInactivityTimeout()).isEqualTo(newInactivityTimeout);
assertThat(deviceState.isActive()).isEqualTo(expectedActivityState);
if (activityState && !expectedActivityState) {
then(telemetrySubscriptionService).should().saveAttributes(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(false)
));
}
}
private static Stream<Arguments> provideParametersForDecreaseInactivityTimeout() {
return Stream.of(
Arguments.of(true, 1, 0, true),
Arguments.of(true, 1, 1, false)
);
}
@Test @Test
public void givenStateDataIsNull_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { void givenStateDataIsNull_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() {
// GIVEN // GIVEN
service.deviceStates.put(deviceId, deviceStateDataMock); service.deviceStates.put(deviceId, deviceStateDataMock);
@ -875,7 +724,7 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenNotMyPartition_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { void givenNotMyPartition_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() {
// GIVEN // GIVEN
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
@ -911,7 +760,7 @@ public class DefaultDeviceStateServiceTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideParametersForUpdateInactivityStateIfExpired") @MethodSource("provideParametersForUpdateInactivityStateIfExpired")
public void givenTestParameters_whenUpdateInactivityStateIfExpired_thenShouldBeInTheExpectedStateAndPerformExpectedActions( void givenTestParameters_whenUpdateInactivityStateIfExpired_thenShouldBeInTheExpectedStateAndPerformExpectedActions(
boolean activityState, long ts, long lastActivityTime, long lastInactivityAlarmTime, long inactivityTimeout, long deviceCreationTime, boolean activityState, long ts, long lastActivityTime, long lastInactivityAlarmTime, long inactivityTimeout, long deviceCreationTime,
boolean expectedActivityState, long expectedLastInactivityAlarmTime, boolean shouldUpdateActivityStateToInactive boolean expectedActivityState, long expectedLastInactivityAlarmTime, boolean shouldUpdateActivityStateToInactive
) { ) {
@ -933,6 +782,7 @@ public class DefaultDeviceStateServiceTest {
if (shouldUpdateActivityStateToInactive) { if (shouldUpdateActivityStateToInactive) {
given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi);
mockSuccessfulSaveAttributes();
} }
// WHEN // WHEN
@ -943,7 +793,7 @@ public class DefaultDeviceStateServiceTest {
assertThat(state.getLastInactivityAlarmTime()).isEqualTo(expectedLastInactivityAlarmTime); assertThat(state.getLastInactivityAlarmTime()).isEqualTo(expectedLastInactivityAlarmTime);
if (shouldUpdateActivityStateToInactive) { if (shouldUpdateActivityStateToInactive) {
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(false) request.getEntries().get(0).getValue().equals(false)
)); ));
@ -961,7 +811,7 @@ public class DefaultDeviceStateServiceTest {
assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId);
assertThat(actualNotification.isActive()).isFalse(); assertThat(actualNotification.isActive()).isFalse();
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) &&
request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getScope().equals(AttributeScope.SERVER_SCOPE) &&
request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) &&
@ -1033,7 +883,79 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { void givenInactiveDevice_whenActivityStatusChangesToActiveButFailedToSaveUpdatedActivityStatus_thenShouldNotUpdateCache2() {
// GIVEN
var deviceState = DeviceState.builder()
.active(false)
.lastActivityTime(100L)
.inactivityTimeout(50L)
.build();
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(TbMsgMetaData.EMPTY)
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
// WHEN-THEN
// simulating short DB outage
given(telemetrySubscriptionService.saveAttributesInternal(any())).willReturn(Futures.immediateFailedFuture(new RuntimeException("failed to save")));
doReturn(200L).when(service).getCurrentTimeMillis();
service.onDeviceActivity(tenantId, deviceId, 180L);
assertThat(deviceState.isActive()).isFalse(); // still inactive
// 10 millis pass... and new activity message it received
// this time DB save is successful
when(telemetrySubscriptionService.saveAttributesInternal(any())).thenReturn(Futures.immediateFuture(AttributesSaveResult.of(generateRandomVersions(1))));
doReturn(210L).when(service).getCurrentTimeMillis();
service.onDeviceActivity(tenantId, deviceId, 190L);
assertThat(deviceState.isActive()).isTrue();
}
@Test
void givenActiveDevice_whenActivityStatusChangesToInactiveButFailedToSaveUpdatedActivityStatus_thenShouldNotUpdateCache() {
// GIVEN
var deviceState = DeviceState.builder()
.active(true)
.lastActivityTime(100L)
.inactivityTimeout(50L)
.build();
var deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(TbMsgMetaData.EMPTY)
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
// WHEN-THEN (assuming periodic activity states check is done every 100 millis)
// simulating short DB outage
given(telemetrySubscriptionService.saveAttributesInternal(any())).willReturn(Futures.immediateFailedFuture(new RuntimeException("failed to save")));
doReturn(200L).when(service).getCurrentTimeMillis();
service.checkStates();
assertThat(deviceState.isActive()).isTrue(); // still active
// waiting 100 millis... periodic activity states check is triggered again
// this time DB save is successful
when(telemetrySubscriptionService.saveAttributesInternal(any())).thenReturn(Futures.immediateFuture(AttributesSaveResult.of(generateRandomVersions(1))));
doReturn(300L).when(service).getCurrentTimeMillis();
service.checkStates();
assertThat(deviceState.isActive()).isFalse();
}
@Test
void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() {
doAnswer(invocation -> { doAnswer(invocation -> {
Thread.sleep(100); Thread.sleep(100);
return deviceStateDataMock; return deviceStateDataMock;
@ -1069,10 +991,8 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException { void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() {
// GIVEN // GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
@ -1086,13 +1006,15 @@ public class DefaultDeviceStateServiceTest {
.setDeleted(false) .setDeleted(false)
.build(); .build();
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.onQueueMsg(proto, TbCallback.EMPTY); service.onQueueMsg(proto, TbCallback.EMPTY);
// THEN // THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false); assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false);
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(false) request.getEntries().get(0).getValue().equals(false)
)); ));
@ -1100,14 +1022,12 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException { void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() {
// GIVEN // GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder() DeviceState deviceState = DeviceState.builder()
.active(false) .active(false)
.inactivityTimeout(service.getDefaultInactivityTimeoutInSec()) .inactivityTimeout(defaultInactivityTimeoutMs)
.build(); .build();
DeviceStateData stateData = DeviceStateData.builder() DeviceStateData stateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
@ -1118,12 +1038,14 @@ public class DefaultDeviceStateServiceTest {
.build(); .build();
service.deviceStates.put(deviceId, stateData); service.deviceStates.put(deviceId, stateData);
mockSuccessfulSaveAttributes();
// WHEN // WHEN
service.onDeviceActivity(tenantId, deviceId, currentTime); service.onDeviceActivity(tenantId, deviceId, currentTime);
// THEN // THEN
ArgumentCaptor<AttributesSaveRequest> attributeRequestCaptor = ArgumentCaptor.forClass(AttributesSaveRequest.class); ArgumentCaptor<AttributesSaveRequest> attributeRequestCaptor = ArgumentCaptor.forClass(AttributesSaveRequest.class);
then(telemetrySubscriptionService).should(times(2)).saveAttributes(attributeRequestCaptor.capture()); then(telemetrySubscriptionService).should(times(2)).saveAttributesInternal(attributeRequestCaptor.capture());
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
@ -1151,15 +1073,14 @@ public class DefaultDeviceStateServiceTest {
} }
@Test @Test
public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException { void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() {
// GIVEN // GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList()));
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder()
var deviceState = DeviceState.builder()
.active(true) .active(true)
.lastConnectTime(currentTime - 8000) .lastConnectTime(currentTime - 8000)
.lastActivityTime(currentTime - 4000) .lastActivityTime(currentTime - 4000)
@ -1167,16 +1088,20 @@ public class DefaultDeviceStateServiceTest {
.lastInactivityAlarmTime(0) .lastInactivityAlarmTime(0)
.inactivityTimeout(3000) .inactivityTimeout(3000)
.build(); .build();
DeviceStateData stateData = DeviceStateData.builder()
var stateData = DeviceStateData.builder()
.tenantId(tenantId) .tenantId(tenantId)
.deviceId(deviceId) .deviceId(deviceId)
.deviceCreationTime(currentTime - 10000) .deviceCreationTime(currentTime - 10000)
.state(deviceState) .state(deviceState)
.build(); .build();
service.deviceStates.put(deviceId, stateData); service.deviceStates.put(deviceId, stateData);
mockSuccessfulSaveAttributes();
// WHEN // WHEN
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() var proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
@ -1190,11 +1115,25 @@ public class DefaultDeviceStateServiceTest {
// THEN // THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request ->
request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) &&
request.getEntries().get(0).getValue().equals(true) request.getEntries().get(0).getValue().equals(true)
)); ));
}); });
} }
private void mockSuccessfulSaveAttributes() {
lenient().when(telemetrySubscriptionService.saveAttributesInternal(any())).thenAnswer(invocation -> {
AttributesSaveRequest request = invocation.getArgument(0);
return Futures.immediateFuture(generateRandomVersions(request.getEntries().size()));
});
}
private static List<Long> generateRandomVersions(int n) {
return ThreadLocalRandom.current()
.longs(n)
.boxed()
.toList();
}
} }

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

@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
@ -472,7 +473,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(saveAttributes, sendWsUpdate, processCalculatedFields)) .strategy(new AttributesSaveRequest.Strategy(saveAttributes, sendWsUpdate, processCalculatedFields))
.build(); .build();
lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries())).thenReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries()))
.thenReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -547,7 +549,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); given(attrService.save(tenantId, deviceId, request.getScope(), entries))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -581,7 +584,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -613,7 +617,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); given(attrService.save(tenantId, deviceId, request.getScope(), entries))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -640,7 +645,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); given(attrService.save(tenantId, deviceId, request.getScope(), entries))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -715,7 +721,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -764,7 +771,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -792,7 +800,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -815,7 +824,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -843,7 +853,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);
@ -870,7 +881,8 @@ class DefaultTelemetrySubscriptionServiceTest {
.strategy(new AttributesSaveRequest.Strategy(true, false, false)) .strategy(new AttributesSaveRequest.Strategy(true, false, false))
.build(); .build();
given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries()))
.willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size()))));
// WHEN // WHEN
telemetryService.saveAttributes(request); telemetryService.saveAttributes(request);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -37,9 +38,9 @@ public interface AttributesService {
ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope); ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope);
ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes); ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes);
ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute);
ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List<String> attributeKeys); ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List<String> attributeKeys);

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

@ -20,6 +20,7 @@ import lombok.Data;
@Data @Data
public class SystemInfoData { public class SystemInfoData {
@Schema(description = "Service Id.") @Schema(description = "Service Id.")
private String serviceId; private String serviceId;
@Schema(description = "Service type.") @Schema(description = "Service type.")

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration {
private boolean preserveMsgTs; private boolean useLatestTs;
@Override @Override
public CalculatedFieldType getType() { public CalculatedFieldType getType() {

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

@ -20,7 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.lang3.BooleanUtils;
import static org.apache.commons.lang3.BooleanUtils.toBooleanDefaultIfNull;
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
@ -34,14 +35,14 @@ public class EdqsState {
private EdqsApiMode apiMode; private EdqsApiMode apiMode;
public boolean updateEdqsReady(boolean ready) { public boolean updateEdqsReady(boolean ready) {
boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready; boolean changed = toBooleanDefaultIfNull(this.edqsReady, false) != ready;
this.edqsReady = ready; this.edqsReady = ready;
return changed; return changed;
} }
@JsonIgnore @JsonIgnore
public boolean isApiReady() { public boolean isApiReady() {
return edqsReady && syncStatus == EdqsSyncStatus.FINISHED; return toBooleanDefaultIfNull(edqsReady, false) && syncStatus == EdqsSyncStatus.FINISHED;
} }
@JsonIgnore @JsonIgnore

11
common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.job.task; package org.thingsboard.server.common.data.job.task;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -25,22 +26,25 @@ import org.thingsboard.server.common.data.job.JobType;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder
@ToString(callSuper = true) @ToString(callSuper = true)
public class DummyTaskResult extends TaskResult { public class DummyTaskResult extends TaskResult {
private DummyTaskFailure failure; private DummyTaskFailure failure;
@Builder
private DummyTaskResult(boolean success, boolean discarded, DummyTaskFailure failure) {
super(success, discarded);
this.failure = failure;
}
public static DummyTaskResult success(DummyTask task) { public static DummyTaskResult success(DummyTask task) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.success(true) .success(true)
.build(); .build();
} }
public static DummyTaskResult failed(DummyTask task, Throwable error) { public static DummyTaskResult failed(DummyTask task, Throwable error) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.failure(DummyTaskFailure.builder() .failure(DummyTaskFailure.builder()
.error(error.getMessage()) .error(error.getMessage())
.number(task.getNumber()) .number(task.getNumber())
@ -51,7 +55,6 @@ public class DummyTaskResult extends TaskResult {
public static DummyTaskResult discarded(DummyTask task) { public static DummyTaskResult discarded(DummyTask task) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.discarded(true) .discarded(true)
.build(); .build();
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java

@ -20,16 +20,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.JobType;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType")
@JsonSubTypes({ @JsonSubTypes({
@ -40,6 +36,12 @@ public abstract class TaskResult {
private String key; private String key;
private boolean success; private boolean success;
private boolean discarded; private boolean discarded;
private long finishTs;
protected TaskResult(boolean success, boolean discarded) {
this.success = success;
this.discarded = discarded;
}
@JsonIgnore @JsonIgnore
public abstract JobType getJobType(); public abstract JobType getJobType();

17
dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java → common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributesSaveResult.java

@ -13,7 +13,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.dao.util; package org.thingsboard.server.common.data.kv;
import java.util.Collections;
import java.util.List;
public record AttributesSaveResult(List<Long> versions) {
public static final AttributesSaveResult EMPTY = new AttributesSaveResult(Collections.emptyList());
public static AttributesSaveResult of(List<Long> versions) {
if (versions == null) {
return EMPTY;
}
return new AttributesSaveResult(versions);
}
public class TenantRateLimitException extends Exception {
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -43,6 +43,7 @@ public enum LimitedApi {
RateLimitUtil.merge( RateLimitUtil.merge(
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits, DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits,
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true), DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true),
CASSANDRA_QUERIES(null, true), // left for backward compatibility with RateLimitsNotificationInfo
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true), EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false), EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true), EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java

@ -30,12 +30,16 @@ public class ResourcesShortageNotificationInfo implements RuleOriginatedNotifica
private String resource; private String resource;
private Long usage; private Long usage;
private String serviceId;
private String serviceType;
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return Map.of( return Map.of(
"resource", resource, "resource", resource,
"usage", String.valueOf(usage) "usage", String.valueOf(usage),
"serviceId", serviceId,
"serviceType", serviceType
); );
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java

@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger { public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 2918443863787603524L;
private final TenantId tenantId; private final TenantId tenantId;
private final CustomerId customerId; private final CustomerId customerId;
private final EdgeId edgeId; private final EdgeId edgeId;
@ -37,8 +41,8 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
private final String error; private final String error;
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override
@ -60,4 +64,5 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
public EntityId getOriginatorEntityId() { public EntityId getOriginatorEntityId() {
return edgeId; return edgeId;
} }
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java

@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class EdgeConnectionTrigger implements NotificationRuleTrigger { public class EdgeConnectionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -261939829962721957L;
private final TenantId tenantId; private final TenantId tenantId;
private final CustomerId customerId; private final CustomerId customerId;
private final EdgeId edgeId; private final EdgeId edgeId;
@ -36,8 +40,8 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
private final String edgeName; private final String edgeName;
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override
@ -59,4 +63,5 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
public EntityId getOriginatorEntityId() { public EntityId getOriginatorEntityId() {
return edgeId; return edgeId;
} }
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java

@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
@Data @Data
@Builder @Builder
public class NewPlatformVersionTrigger implements NotificationRuleTrigger { public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 3298785969736390092L;
private final UpdateMessage updateInfo; private final UpdateMessage updateInfo;
@Override @Override
@ -45,8 +50,8 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override

11
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java

@ -29,9 +29,8 @@ public interface NotificationRuleTrigger extends Serializable {
EntityId getOriginatorEntityId(); EntityId getOriginatorEntityId();
default DeduplicationStrategy getDeduplicationStrategy() {
default boolean deduplicate() { return DeduplicationStrategy.NONE;
return false;
} }
default String getDeduplicationKey() { default String getDeduplicationKey() {
@ -43,4 +42,10 @@ public interface NotificationRuleTrigger extends Serializable {
return 0; return 0;
} }
enum DeduplicationStrategy {
NONE,
ALL,
ONLY_MATCHING
}
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java

@ -22,12 +22,16 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class RateLimitsTrigger implements NotificationRuleTrigger { public class RateLimitsTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -4423112145409424886L;
private final TenantId tenantId; private final TenantId tenantId;
private final LimitedApi api; private final LimitedApi api;
private final EntityId limitLevel; private final EntityId limitLevel;
@ -45,8 +49,8 @@ public class RateLimitsTrigger implements NotificationRuleTrigger {
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java

@ -33,6 +33,8 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
private Resource resource; private Resource resource;
private Long usage; private Long usage;
private String serviceId;
private String serviceType;
@Override @Override
public TenantId getTenantId() { public TenantId getTenantId() {
@ -45,13 +47,13 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
} }
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ONLY_MATCHING;
} }
@Override @Override
public String getDeduplicationKey() { public String getDeduplicationKey() {
return resource.name(); return String.join(":", resource.name(), serviceId, serviceType);
} }
@Override @Override

1
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -277,6 +277,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
eventConsumer.awaitStop(); eventConsumer.awaitStop();
responseTemplate.stop(); responseTemplate.stop();
stateService.stop(); stateService.stop();
versionsStore.shutdown();
} }
} }

1
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -224,6 +224,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
stateConsumer.awaitStop(); stateConsumer.awaitStop();
eventsToBackupConsumer.stop(); eventsToBackupConsumer.stop();
stateProducer.stop(); stateProducer.stop();
versionsStore.shutdown();
} }
} }

51
common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java

@ -15,31 +15,35 @@
*/ */
package org.thingsboard.server.edqs.util; package org.thingsboard.server.edqs.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edqs.EdqsObjectKey; import org.thingsboard.server.common.data.edqs.EdqsObjectKey;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j @Slf4j
public class VersionsStore { public class VersionsStore {
private final Cache<EdqsObjectKey, Long> versions; private final ConcurrentMap<EdqsObjectKey, TimedValue> versions = new ConcurrentHashMap<>();
private final long expirationMillis;
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
public VersionsStore(int ttlMinutes) { public VersionsStore(int ttlMinutes) {
this.versions = Caffeine.newBuilder() this.expirationMillis = TimeUnit.MINUTES.toMillis(ttlMinutes);
.expireAfterWrite(ttlMinutes, TimeUnit.MINUTES) startCleanupTask();
.build();
} }
public boolean isNew(EdqsObjectKey key, Long version) { public boolean isNew(EdqsObjectKey key, Long version) {
AtomicBoolean isNew = new AtomicBoolean(false); AtomicBoolean isNew = new AtomicBoolean(false);
versions.asMap().compute(key, (k, prevVersion) -> { versions.compute(key, (k, prevVersion) -> {
if (prevVersion == null || prevVersion <= version) { if (prevVersion == null || prevVersion.value <= version) {
isNew.set(true); isNew.set(true);
return version; return new TimedValue(version);
} else { } else {
log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion);
return prevVersion; return prevVersion;
@ -48,4 +52,33 @@ public class VersionsStore {
return isNew.get(); return isNew.get();
} }
private void startCleanupTask() {
cleaner.scheduleAtFixedRate(() -> {
try {
long now = System.currentTimeMillis();
for (Map.Entry<EdqsObjectKey, TimedValue> entry : versions.entrySet()) {
if (now - entry.getValue().lastUpdated > expirationMillis) {
versions.remove(entry.getKey(), entry.getValue());
}
}
} catch (Exception e) {
log.error("Cleanup task failed", e);
}
}, expirationMillis, expirationMillis, TimeUnit.MILLISECONDS);
}
public void shutdown() {
cleaner.shutdown();
}
private static class TimedValue {
private final long lastUpdated;
private final long value;
public TimedValue(long value) {
this.value = value;
this.lastUpdated = System.currentTimeMillis();
}
}
} }

3
common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java

@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -47,7 +48,7 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso
@Override @Override
public void process(NotificationRuleTrigger trigger) { public void process(NotificationRuleTrigger trigger) {
try { try {
if (trigger.deduplicate() && deduplicationService.alreadyProcessed(trigger)) { if (!DeduplicationStrategy.NONE.equals(trigger.getDeduplicationStrategy()) && deduplicationService.alreadyProcessed(trigger)) {
return; return;
} }

2
common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java

@ -232,6 +232,8 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
} }
private void reportTaskResult(T task, R result) { private void reportTaskResult(T task, R result) {
result.setKey(task.getKey());
result.setFinishTs(System.currentTimeMillis());
statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result);
} }

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

@ -25,11 +25,11 @@ public class TbelCfCtx implements TbelCfObject {
@Getter @Getter
private final Map<String, TbelCfArg> args; private final Map<String, TbelCfArg> args;
@Getter @Getter
private final long msgTs; private final long latestTs;
public TbelCfCtx(Map<String, TbelCfArg> args, long lastUpdateTs) { public TbelCfCtx(Map<String, TbelCfArg> args, long latestTs) {
this.args = Collections.unmodifiableMap(args); this.args = Collections.unmodifiableMap(args);
this.msgTs = lastUpdateTs != -1 ? lastUpdateTs : System.currentTimeMillis(); this.latestTs = latestTs != -1 ? latestTs : System.currentTimeMillis();
} }
@Override @Override

27
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
@ -41,7 +42,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
@ -101,26 +101,29 @@ public class BaseAttributesService implements AttributesService {
} }
@Override @Override
public ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { public ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attribute, valueNoXssValidation); AttributeUtils.validate(attribute, valueNoXssValidation);
return doSave(tenantId, entityId, scope, attribute); return doSave(tenantId, entityId, scope, List.of(attribute));
} }
@Override @Override
public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) { public ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attributes, valueNoXssValidation); AttributeUtils.validate(attributes, valueNoXssValidation);
List<ListenableFuture<Long>> saveFutures = attributes.stream().map(attribute -> doSave(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return doSave(tenantId, entityId, scope, attributes);
return Futures.allAsList(saveFutures);
} }
private ListenableFuture<Long> doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { private ListenableFuture<AttributesSaveResult> doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
ListenableFuture<Long> future = attributesDao.save(tenantId, entityId, scope, attribute); List<ListenableFuture<Long>> futures = new ArrayList<>(attributes.size());
return Futures.transform(future, version -> { for (AttributeKvEntry attribute : attributes) {
edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); ListenableFuture<Long> future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> {
return version; edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version));
}, MoreExecutors.directExecutor()); return version;
}, MoreExecutors.directExecutor());
futures.add(future);
}
return Futures.transform(Futures.allAsList(futures), AttributesSaveResult::of, MoreExecutors.directExecutor());
} }
@Override @Override

38
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.edqs.EdqsService;
@ -56,7 +57,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
@ -150,7 +150,7 @@ public class CachedAttributesService implements AttributesService {
List<AttributeKvEntry> cachedAttributes = wrappedCachedAttributes.values().stream() List<AttributeKvEntry> cachedAttributes = wrappedCachedAttributes.values().stream()
.map(TbCacheValueWrapper::get) .map(TbCacheValueWrapper::get)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .toList();
if (wrappedCachedAttributes.size() == attributeKeys.size()) { if (wrappedCachedAttributes.size() == attributeKeys.size()) {
log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys); log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys);
return Futures.immediateFuture(cachedAttributes); return Futures.immediateFuture(cachedAttributes);
@ -159,8 +159,6 @@ public class CachedAttributesService implements AttributesService {
Set<String> notFoundAttributeKeys = new HashSet<>(attributeKeys); Set<String> notFoundAttributeKeys = new HashSet<>(attributeKeys);
notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
List<AttributeCacheKey> notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList());
// DB call should run in DB executor, not in cache-related executor // DB call should run in DB executor, not in cache-related executor
return jpaExecutorService.submit(() -> { return jpaExecutorService.submit(() -> {
log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys);
@ -222,33 +220,31 @@ public class CachedAttributesService implements AttributesService {
} }
@Override @Override
public ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { public ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attribute, valueNoXssValidation); AttributeUtils.validate(attribute, valueNoXssValidation);
return doSave(tenantId, entityId, scope, attribute); return doSave(tenantId, entityId, scope, List.of(attribute));
} }
@Override @Override
public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) { public ListenableFuture<AttributesSaveResult> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attributes, valueNoXssValidation); AttributeUtils.validate(attributes, valueNoXssValidation);
return doSave(tenantId, entityId, scope, attributes);
}
private ListenableFuture<AttributesSaveResult> doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
List<ListenableFuture<Long>> futures = new ArrayList<>(attributes.size()); List<ListenableFuture<Long>> futures = new ArrayList<>(attributes.size());
for (var attribute : attributes) { for (var attribute : attributes) {
futures.add(doSave(tenantId, entityId, scope, attribute)); ListenableFuture<Long> future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> {
BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version);
put(entityId, scope, attributeKvEntry);
edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version));
return version;
}, cacheExecutor);
futures.add(future);
} }
return Futures.transform(Futures.allAsList(futures), AttributesSaveResult::of, MoreExecutors.directExecutor());
return Futures.allAsList(futures);
}
private ListenableFuture<Long> doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
ListenableFuture<Long> future = attributesDao.save(tenantId, entityId, scope, attribute);
return Futures.transform(future, version -> {
BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version);
put(entityId, scope, attributeKvEntry);
edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version));
return version;
}, cacheExecutor);
} }
private void put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { private void put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
@ -270,7 +266,7 @@ public class CachedAttributesService implements AttributesService {
edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version));
} }
return key; return key;
}, cacheExecutor)).collect(Collectors.toList())); }, cacheExecutor)).toList());
} }
@Override @Override

10
dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

@ -69,7 +69,6 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(QUEUED); job.setStatus(QUEUED);
} else { } else {
job.setStatus(PENDING); job.setStatus(PENDING);
job.getResult().setStartTs(System.currentTimeMillis());
} }
return saveJob(tenantId, job, true, null); return saveJob(tenantId, job, true, null);
} }
@ -125,6 +124,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
} }
boolean publishEvent = false; boolean publishEvent = false;
long lastFinishTs = 0;
for (TaskResult taskResult : jobStats.getTaskResults()) { for (TaskResult taskResult : jobStats.getTaskResults()) {
if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) { if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) {
log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey()); log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey());
@ -140,6 +140,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
publishEvent = true; publishEvent = true;
} }
} }
if (taskResult.getFinishTs() > lastFinishTs) {
lastFinishTs = taskResult.getFinishTs();
}
} }
if (job.getStatus() == RUNNING) { if (job.getStatus() == RUNNING) {
@ -153,7 +156,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(COMPLETED); job.setStatus(COMPLETED);
publishEvent = true; publishEvent = true;
} }
result.setFinishTs(System.currentTimeMillis()); result.setFinishTs(lastFinishTs);
job.getConfiguration().setToReprocess(null); job.getConfiguration().setToReprocess(null);
} }
} }
@ -166,6 +169,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) { if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) {
throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType()); throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType());
} }
if (job.getStatus() == PENDING) {
job.getResult().setStartTs(System.currentTimeMillis());
}
job = jobDao.save(tenantId, job); job = jobDao.save(tenantId, job);
if (publishEvent) { if (publishEvent) {

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

Loading…
Cancel
Save