diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
index 9adeb4f49e..6701b59e0e 100644
--- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
+++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
@@ -33,8 +33,13 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
+ "configurationVersion": 1,
"configuration": {
- "defaultTTL": 0
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
},
"externalId": null
},
@@ -185,4 +190,4 @@
],
"ruleChainConnections": null
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg b/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg
new file mode 100644
index 0000000000..f6d881b172
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg
@@ -0,0 +1,548 @@
+
+220 220 220 v
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/apartments-hp.svg b/application/src/main/data/json/system/scada_symbols/apartments-hp.svg
new file mode 100644
index 0000000000..9e2c965f79
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/apartments-hp.svg
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/battery-hp.svg b/application/src/main/data/json/system/scada_symbols/battery-hp.svg
new file mode 100644
index 0000000000..8b63a9ab29
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/battery-hp.svg
@@ -0,0 +1,473 @@
+
+
+
+
+
+
+
+
+
+ ON
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg
new file mode 100644
index 0000000000..4f0603b904
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-light-bulb-hp.svg
@@ -0,0 +1,346 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/consumers-hp.svg b/application/src/main/data/json/system/scada_symbols/consumers-hp.svg
new file mode 100644
index 0000000000..e4005d7ae2
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/consumers-hp.svg
@@ -0,0 +1,346 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg b/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg
new file mode 100644
index 0000000000..12f8b4944f
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg
@@ -0,0 +1,444 @@
+ {\n ctx.api.setValue('initialState', !initial);\n }\n});"
+ }
+ }
+ },
+ {
+ "tag": "breaker-trigger",
+ "stateRenderFunction": "element.fill(ctx.properties.disabledColor);\nif (ctx.values.initialState) {\n element.transform({translateY: 0});\n} else {\n element.transform({translateY: 160});\n}",
+ "actions": null
+ },
+ {
+ "tag": "critical",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.criticalColor});\nif (ctx.values.critical) {\n element.show();\n} else {\n element.hide();\n}\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\n",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'criticalClick');"
+ }
+ }
+ },
+ {
+ "tag": "label",
+ "stateRenderFunction": "if (ctx.properties.label) {\n element.show();\n var label = ctx.values.initialState ? ctx.properties.onLabel : ctx.properties.offLabel;\n ctx.api.font(element, ctx.properties.labelFont, ctx.properties.labelColor);\n ctx.api.text(element, label);\n} else {\n element.hide();\n}",
+ "actions": null
+ },
+ {
+ "tag": "label-box",
+ "stateRenderFunction": "var color = ctx.properties.disabledColor;\nif (ctx.values.initialState) {\n color = ctx.properties.enabledColor;\n}\nelement.attr({fill: color});",
+ "actions": null
+ },
+ {
+ "tag": "warning",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.warningColor});\nvar warning = ctx.values.warning && !(ctx.values.warning && ctx.values.critical)\nif (warning) {\n element.show();\n} else {\n element.hide();\n}\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = warning && 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",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'warningClick');"
+ }
+ }
+ }
+ ],
+ "behavior": [
+ {
+ "id": "initialState",
+ "name": "{i18n:scada.symbol.on-off-state}",
+ "hint": "{i18n:scada.symbol.on-off-state-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": "{i18n:scada.symbol.on}",
+ "falseLabel": "{i18n:scada.symbol.off}",
+ "stateLabel": "",
+ "defaultGetValueSettings": {
+ "action": "GET_ATTRIBUTE",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": null,
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "onUpdateState",
+ "name": "{i18n:scada.symbol.on-update-state}",
+ "hint": "{i18n:scada.symbol.on-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SERVER_SCOPE",
+ "key": "state"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "offUpdateState",
+ "name": "{i18n:scada.symbol.off-update-state}",
+ "hint": "{i18n:scada.symbol.off-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "key": "state",
+ "scope": "SERVER_SCOPE"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warning",
+ "name": "{i18n:scada.symbol.warning}",
+ "hint": "{i18n:scada.symbol.warning-state-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.warning}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "MAJOR",
+ "MINOR",
+ "WARNING",
+ "INDETERMINATE"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warningClick",
+ "name": "{i18n:scada.symbol.warning-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "critical",
+ "name": "{i18n:scada.symbol.critical}",
+ "hint": "{i18n:scada.symbol.critical-state-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.critical}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "CRITICAL"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "criticalClick",
+ "name": "{i18n:scada.symbol.critical-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "criticalAnimation",
+ "name": "{i18n:scada.symbol.warning-critical-state-animation}",
+ "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.animation}",
+ "defaultGetValueSettings": {
+ "action": "DO_NOTHING",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "key": "state",
+ "scope": null
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "compareToValue": true,
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;"
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ }
+ ],
+ "properties": [
+ {
+ "id": "label",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "switch",
+ "default": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "onLabel",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "text",
+ "default": "ON",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "offLabel",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "text",
+ "default": "OFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "labelFont",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "font",
+ "default": {
+ "size": 42,
+ "sizeUnit": "px",
+ "family": "Roboto",
+ "weight": "400",
+ "style": "normal"
+ },
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "labelColor",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "color",
+ "default": "#1A1A1A",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "backgroundColor",
+ "name": "{i18n:scada.symbol.background-color}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "enabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "subLabel": "{i18n:scada.symbol.enabled}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "disabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#666666",
+ "subLabel": "{i18n:scada.symbol.disabled}",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "warningColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#FAA405",
+ "subLabel": "{i18n:scada.symbol.warning}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "criticalColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#D12730",
+ "subLabel": "{i18n:scada.symbol.critical}",
+ "disabled": false,
+ "visible": true
+ }
+ ]
+}]]>
+
+
+
+ ON
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg b/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg
new file mode 100644
index 0000000000..0fcfe4dde0
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/electrical-distribution-board-hp.svg
@@ -0,0 +1,332 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg
new file mode 100644
index 0000000000..606eb1153d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg
@@ -0,0 +1,482 @@
+
+000023 kWh T1
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg
new file mode 100644
index 0000000000..de4fb13836
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg
@@ -0,0 +1,876 @@
+
+T1 T2 T3 Export 000223 000223 000223 000223 kWh
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/fuel-generator-hp.svg b/application/src/main/data/json/system/scada_symbols/fuel-generator-hp.svg
new file mode 100644
index 0000000000..52a3c70d6e
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/fuel-generator-hp.svg
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/high-voltage-tower-hp.svg b/application/src/main/data/json/system/scada_symbols/high-voltage-tower-hp.svg
new file mode 100644
index 0000000000..3aef3ff948
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/high-voltage-tower-hp.svg
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg
new file mode 100644
index 0000000000..17fd1ba69e
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg
@@ -0,0 +1,439 @@
+ {\n ctx.api.setValue('initialState', !initial);\n }\n});"
+ }
+ }
+ },
+ {
+ "tag": "breaker-trigger",
+ "stateRenderFunction": "element.fill(ctx.properties.disabledColor);\nif (ctx.values.initialState) {\n element.transform({translateX: 0});\n} else {\n element.transform({translateX: -160});\n}",
+ "actions": null
+ },
+ {
+ "tag": "critical",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.criticalColor});\nif (ctx.values.critical) {\n element.show();\n} else {\n element.hide();\n}\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\n",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'criticalClick');"
+ }
+ }
+ },
+ {
+ "tag": "label",
+ "stateRenderFunction": "if (ctx.properties.label) {\n element.show();\n var label = ctx.values.initialState ? ctx.properties.onLabel : ctx.properties.offLabel;\n ctx.api.font(element, ctx.properties.labelFont, ctx.properties.labelColor);\n ctx.api.text(element, label);\n} else {\n element.hide();\n}",
+ "actions": null
+ },
+ {
+ "tag": "label-box",
+ "stateRenderFunction": "var color = ctx.properties.disabledColor;\nif (ctx.values.initialState) {\n color = ctx.properties.enabledColor;\n}\nelement.attr({fill: color});",
+ "actions": null
+ },
+ {
+ "tag": "warning",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.warningColor});\nvar warning = ctx.values.warning && !(ctx.values.warning && ctx.values.critical)\nif (warning) {\n element.show();\n} else {\n element.hide();\n}\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = warning && 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",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'warningClick');"
+ }
+ }
+ }
+ ],
+ "behavior": [
+ {
+ "id": "initialState",
+ "name": "{i18n:scada.symbol.on-off-state}",
+ "hint": "{i18n:scada.symbol.on-off-state-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": "{i18n:scada.symbol.on}",
+ "falseLabel": "{i18n:scada.symbol.off}",
+ "stateLabel": "",
+ "defaultGetValueSettings": {
+ "action": "GET_ATTRIBUTE",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": null,
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "onUpdateState",
+ "name": "{i18n:scada.symbol.on-update-state}",
+ "hint": "{i18n:scada.symbol.on-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SERVER_SCOPE",
+ "key": "state"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "offUpdateState",
+ "name": "{i18n:scada.symbol.off-update-state}",
+ "hint": "{i18n:scada.symbol.off-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "key": "state",
+ "scope": "SERVER_SCOPE"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warning",
+ "name": "{i18n:scada.symbol.warning}",
+ "hint": "{i18n:scada.symbol.warning-state-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.warning}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "MAJOR",
+ "MINOR",
+ "WARNING",
+ "INDETERMINATE"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warningClick",
+ "name": "{i18n:scada.symbol.warning-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "critical",
+ "name": "{i18n:scada.symbol.critical}",
+ "hint": "{i18n:scada.symbol.critical-state-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.critical}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "CRITICAL"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "criticalClick",
+ "name": "{i18n:scada.symbol.critical-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "criticalAnimation",
+ "name": "{i18n:scada.symbol.warning-critical-state-animation}",
+ "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.animation}",
+ "defaultGetValueSettings": {
+ "action": "DO_NOTHING",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "key": "state",
+ "scope": null
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "compareToValue": true,
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;"
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ }
+ ],
+ "properties": [
+ {
+ "id": "label",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "switch",
+ "default": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "onLabel",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "text",
+ "default": "ON",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "offLabel",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "text",
+ "default": "OFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "labelFont",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "font",
+ "default": {
+ "size": 42,
+ "sizeUnit": "px",
+ "family": "Roboto",
+ "weight": "400",
+ "style": "normal"
+ },
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "labelColor",
+ "name": "{i18n:scada.symbol.label}",
+ "type": "color",
+ "default": "#1A1A1A",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "backgroundColor",
+ "name": "{i18n:scada.symbol.background-color}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "enabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "subLabel": "{i18n:scada.symbol.enabled}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "disabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#666666",
+ "subLabel": "{i18n:scada.symbol.disabled}",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "warningColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#FAA405",
+ "subLabel": "{i18n:scada.symbol.warning}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "criticalColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#D12730",
+ "subLabel": "{i18n:scada.symbol.critical}",
+ "disabled": false,
+ "visible": true
+ }
+ ]
+}]]>
+
+
+ ON
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg
new file mode 100644
index 0000000000..13ee85821b
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg
@@ -0,0 +1,378 @@
+
+Connected
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/house-hp.svg b/application/src/main/data/json/system/scada_symbols/house-hp.svg
new file mode 100644
index 0000000000..d95c47bb8b
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/house-hp.svg
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg b/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg
new file mode 100644
index 0000000000..2b7a476a92
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/industrial-fuel-generator-hp.svg
@@ -0,0 +1,358 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/inverter-hp.svg b/application/src/main/data/json/system/scada_symbols/inverter-hp.svg
new file mode 100644
index 0000000000..73f14c8e75
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/inverter-hp.svg
@@ -0,0 +1,578 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg b/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg
new file mode 100644
index 0000000000..11bc5da0a6
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/large-inverter-hp.svg
@@ -0,0 +1,578 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg b/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg
new file mode 100644
index 0000000000..812e616433
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/low-voltage-tower-hp.svg
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/low-voltage-transformer-tower-hp.svg b/application/src/main/data/json/system/scada_symbols/low-voltage-transformer-tower-hp.svg
new file mode 100644
index 0000000000..2985dc600c
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/low-voltage-transformer-tower-hp.svg
@@ -0,0 +1,334 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/manufacture-hp.svg b/application/src/main/data/json/system/scada_symbols/manufacture-hp.svg
new file mode 100644
index 0000000000..5b35f2eaf2
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/manufacture-hp.svg
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg b/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg
new file mode 100644
index 0000000000..5524b3bb14
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/power-socket-hp.svg
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/power-transformer-hp.svg b/application/src/main/data/json/system/scada_symbols/power-transformer-hp.svg
new file mode 100644
index 0000000000..67e75be827
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/power-transformer-hp.svg
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/single-key-switch-hp.svg b/application/src/main/data/json/system/scada_symbols/single-key-switch-hp.svg
new file mode 100644
index 0000000000..77d6e7f95d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/single-key-switch-hp.svg
@@ -0,0 +1,373 @@
+ {\n ctx.api.setValue('initialState', !initial);\n }\n});"
+ }
+ }
+ },
+ {
+ "tag": "critical",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.criticalColor});\nif (ctx.values.critical) {\n element.show();\n} else {\n element.hide();\n}\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\n",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'criticalClick');"
+ }
+ }
+ },
+ {
+ "tag": "warning",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.warningColor});\nvar warning = ctx.values.warning && !(ctx.values.warning && ctx.values.critical)\nif (warning) {\n element.show();\n} else {\n element.hide();\n}\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = warning && 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",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'warningClick');"
+ }
+ }
+ }
+ ],
+ "behavior": [
+ {
+ "id": "initialState",
+ "name": "{i18n:scada.symbol.on-off-state}",
+ "hint": "{i18n:scada.symbol.on-off-state-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": "{i18n:scada.symbol.on}",
+ "falseLabel": "{i18n:scada.symbol.off}",
+ "stateLabel": "",
+ "defaultGetValueSettings": {
+ "action": "GET_ATTRIBUTE",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": null,
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "onUpdateState",
+ "name": "{i18n:scada.symbol.on-update-state}",
+ "hint": "{i18n:scada.symbol.on-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SERVER_SCOPE",
+ "key": "state"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "offUpdateState",
+ "name": "{i18n:scada.symbol.off-update-state}",
+ "hint": "{i18n:scada.symbol.off-update-state-hint}",
+ "group": null,
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "key": "state",
+ "scope": "SERVER_SCOPE"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warning",
+ "name": "{i18n:scada.symbol.warning}",
+ "hint": "{i18n:scada.symbol.warning-state-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.warning}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "MAJOR",
+ "MINOR",
+ "WARNING",
+ "INDETERMINATE"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warningClick",
+ "name": "{i18n:scada.symbol.warning-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "critical",
+ "name": "{i18n:scada.symbol.critical}",
+ "hint": "{i18n:scada.symbol.critical-state-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.critical}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "CRITICAL"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "criticalClick",
+ "name": "{i18n:scada.symbol.critical-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "criticalAnimation",
+ "name": "{i18n:scada.symbol.warning-critical-state-animation}",
+ "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.animation}",
+ "defaultGetValueSettings": {
+ "action": "DO_NOTHING",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "key": "state",
+ "scope": null
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "compareToValue": true,
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;"
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ }
+ ],
+ "properties": [
+ {
+ "id": "backgroundColor",
+ "name": "{i18n:scada.symbol.background-color}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "enabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "subLabel": "{i18n:scada.symbol.enabled}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "disabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#666666",
+ "subLabel": "{i18n:scada.symbol.disabled}",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "warningColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#FAA405",
+ "subLabel": "{i18n:scada.symbol.warning}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "criticalColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#D12730",
+ "subLabel": "{i18n:scada.symbol.critical}",
+ "disabled": false,
+ "visible": true
+ }
+ ]
+}]]>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/small-power-transformer-hp.svg b/application/src/main/data/json/system/scada_symbols/small-power-transformer-hp.svg
new file mode 100644
index 0000000000..049c75ab58
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-power-transformer-hp.svg
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/solar-panel-hp.svg b/application/src/main/data/json/system/scada_symbols/solar-panel-hp.svg
new file mode 100644
index 0000000000..b7b7724f89
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/solar-panel-hp.svg
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/stand-solar-panel-hp.svg b/application/src/main/data/json/system/scada_symbols/stand-solar-panel-hp.svg
new file mode 100644
index 0000000000..93e622a021
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-solar-panel-hp.svg
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg
new file mode 100644
index 0000000000..f2b7bf4d54
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg
@@ -0,0 +1,747 @@
+
+T1 T2 T3 000223 000223 000223 kWh
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg b/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg
new file mode 100644
index 0000000000..ed6855884a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-light-bulb-hp.svg
@@ -0,0 +1,346 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/two-key-switch-hp.svg b/application/src/main/data/json/system/scada_symbols/two-key-switch-hp.svg
new file mode 100644
index 0000000000..40e04a3bf5
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/two-key-switch-hp.svg
@@ -0,0 +1,489 @@
+ {\n ctx.api.setValue('initialStateLeftButton', !initial);\n }\n});"
+ }
+ }
+ },
+ {
+ "tag": "right-button",
+ "stateRenderFunction": "var color = ctx.properties.disabledColor;\nif (ctx.values.initialStateRightButton) {\n color = ctx.properties.enabledColor;\n}\nelement.attr({fill: color});",
+ "actions": {
+ "click": {
+ "actionFunction": "var initial = ctx.values.initialStateRightButton;\nvar action = initial ? 'offUpdateStateRightButton' : 'onUpdateStateRightButton';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('initialStateRightButton', !initial);\n }\n});"
+ }
+ }
+ },
+ {
+ "tag": "warning",
+ "stateRenderFunction": "element.attr({fill: ctx.properties.warningColor});\nvar warning = ctx.values.warning && !(ctx.values.warning && ctx.values.critical)\nif (warning) {\n element.show();\n} else {\n element.hide();\n}\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = warning && 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",
+ "actions": {
+ "click": {
+ "actionFunction": "ctx.api.callAction(event, 'warningClick');"
+ }
+ }
+ }
+ ],
+ "behavior": [
+ {
+ "id": "initialStateLeftButton",
+ "name": "{i18n:scada.symbol.on-off-state}",
+ "hint": "{i18n:scada.symbol.on-off-state-hint}",
+ "group": "{i18n:scada.symbol.left-button}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": "{i18n:scada.symbol.on}",
+ "falseLabel": "{i18n:scada.symbol.off}",
+ "stateLabel": "",
+ "defaultGetValueSettings": {
+ "action": "GET_ATTRIBUTE",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "leftButton"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": null,
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "onUpdateStateLeftButton",
+ "name": "{i18n:scada.symbol.on-update-state}",
+ "hint": "{i18n:scada.symbol.on-update-state-hint}",
+ "group": "{i18n:scada.symbol.left-button}",
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SERVER_SCOPE",
+ "key": "state"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "offUpdateStateLeftButton",
+ "name": "{i18n:scada.symbol.off-update-state}",
+ "hint": "{i18n:scada.symbol.off-update-state-hint}",
+ "group": "{i18n:scada.symbol.left-button}",
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "key": "state",
+ "scope": "SERVER_SCOPE"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "initialStateRightButton",
+ "name": "{i18n:scada.symbol.on-off-state}",
+ "hint": "{i18n:scada.symbol.on-off-state-hint}",
+ "group": "{i18n:scada.symbol.right-button}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": "{i18n:scada.symbol.on}",
+ "falseLabel": "{i18n:scada.symbol.off}",
+ "stateLabel": null,
+ "defaultGetValueSettings": {
+ "action": "GET_ATTRIBUTE",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": "SHARED_SCOPE",
+ "key": "rightButton"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": null,
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "onUpdateStateRightButton",
+ "name": "{i18n:scada.symbol.on-update-state}",
+ "hint": "{i18n:scada.symbol.on-update-state-hint}",
+ "group": "{i18n:scada.symbol.right-button}",
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "scope": "SERVER_SCOPE",
+ "key": "state"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": true,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "offUpdateStateRightButton",
+ "name": "{i18n:scada.symbol.off-update-state}",
+ "hint": "{i18n:scada.symbol.off-update-state-hint}",
+ "group": "{i18n:scada.symbol.right-button}",
+ "type": "action",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": {
+ "action": "EXECUTE_RPC",
+ "executeRpc": {
+ "method": "setState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "setAttribute": {
+ "key": "state",
+ "scope": "SERVER_SCOPE"
+ },
+ "putTimeSeries": {
+ "key": "state"
+ },
+ "valueToData": {
+ "type": "CONSTANT",
+ "constantValue": false,
+ "valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
+ }
+ },
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warning",
+ "name": "{i18n:scada.symbol.warning}",
+ "hint": "{i18n:scada.symbol.warning-state-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.warning}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "MAJOR",
+ "MINOR",
+ "WARNING",
+ "INDETERMINATE"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "warningClick",
+ "name": "{i18n:scada.symbol.warning-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.warning-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "critical",
+ "name": "{i18n:scada.symbol.critical}",
+ "hint": "{i18n:scada.symbol.critical-state-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.critical}",
+ "defaultGetValueSettings": {
+ "action": "GET_ALARM_STATUS",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "scope": null,
+ "key": "state"
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "getAlarmStatus": {
+ "severityList": [
+ "CRITICAL"
+ ],
+ "typeList": null
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;",
+ "compareToValue": true
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ },
+ {
+ "id": "criticalClick",
+ "name": "{i18n:scada.symbol.critical-click}",
+ "hint": "{i18n:scada.symbol.on-click-hint}",
+ "group": "{i18n:scada.symbol.critical-state}",
+ "type": "widgetAction",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": null,
+ "defaultGetValueSettings": null,
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": {
+ "type": "doNothing",
+ "targetDashboardStateId": null,
+ "openRightLayout": false,
+ "setEntityId": false,
+ "stateEntityParamName": null
+ }
+ },
+ {
+ "id": "criticalAnimation",
+ "name": "{i18n:scada.symbol.warning-critical-state-animation}",
+ "hint": "{i18n:scada.symbol.warning-critical-state-animation-hint}",
+ "group": null,
+ "type": "value",
+ "valueType": "BOOLEAN",
+ "trueLabel": null,
+ "falseLabel": null,
+ "stateLabel": "{i18n:scada.symbol.animation}",
+ "defaultGetValueSettings": {
+ "action": "DO_NOTHING",
+ "defaultValue": false,
+ "executeRpc": {
+ "method": "getState",
+ "requestTimeout": 5000,
+ "requestPersistent": false,
+ "persistentPollingInterval": 1000
+ },
+ "getAttribute": {
+ "key": "state",
+ "scope": null
+ },
+ "getTimeSeries": {
+ "key": "state"
+ },
+ "dataToValue": {
+ "type": "NONE",
+ "compareToValue": true,
+ "dataToValueFunction": "/* Should return boolean value */\nreturn data;"
+ }
+ },
+ "defaultSetValueSettings": null,
+ "defaultWidgetActionSettings": null
+ }
+ ],
+ "properties": [
+ {
+ "id": "backgroundColor",
+ "name": "{i18n:scada.symbol.background-color}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "enabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#FFFFFF",
+ "subLabel": "{i18n:scada.symbol.enabled}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "disabledColor",
+ "name": "{i18n:scada.symbol.colors}",
+ "type": "color",
+ "default": "#666666",
+ "subLabel": "{i18n:scada.symbol.disabled}",
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "warningColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#FAA405",
+ "subLabel": "{i18n:scada.symbol.warning}",
+ "divider": true,
+ "disabled": false,
+ "visible": true
+ },
+ {
+ "id": "criticalColor",
+ "name": "{i18n:scada.symbol.alarm-colors}",
+ "type": "color",
+ "default": "#D12730",
+ "subLabel": "{i18n:scada.symbol.critical}",
+ "disabled": false,
+ "visible": true
+ }
+ ]
+}]]>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg
new file mode 100644
index 0000000000..7adc401836
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg
@@ -0,0 +1,618 @@
+
+T1 T2 000023 000023 kWh
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg
new file mode 100644
index 0000000000..cebe949c36
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg
@@ -0,0 +1,378 @@
+
+Connected
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg b/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg
new file mode 100644
index 0000000000..eaa0b02455
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg
@@ -0,0 +1,439 @@
+
+220 v
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg
new file mode 100644
index 0000000000..aafc2af5a0
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg
@@ -0,0 +1,584 @@
+
+220 230 in out
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg b/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg
new file mode 100644
index 0000000000..74855dd35a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/wind-turbine-cluster-hp.svg
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg b/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg
new file mode 100644
index 0000000000..a4282c16e7
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/wind-turbine-hp.svg
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json
index 5083384432..33efaacc0c 100644
--- a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json
+++ b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json
@@ -28,6 +28,10 @@
"hp_left_tee_connector",
"hp_top_tee_connector",
"hp_drawwork",
- "hp_crane"
+ "hp_crane",
+ "hp_consumers",
+ "hp_house",
+ "hp_apartments",
+ "hp_manufacture"
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/high_performance_scada_energy_system.json b/application/src/main/data/json/system/widget_bundles/high_performance_scada_energy_system.json
new file mode 100644
index 0000000000..a06ccc0819
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/high_performance_scada_energy_system.json
@@ -0,0 +1,48 @@
+{
+ "widgetsBundle": {
+ "alias": "high_performance_scada_energy_system",
+ "title": "High-performance SCADA energy system",
+ "image": "tb-image:aHBfc2NhZGFfZW5lcmd5X3N5c3RlbV9idW5kbGVfaW1hZ2UucG5n:IkhpZ2gtcGVyZm9ybWFuY2UgU0NBREEgZW5lcmd5IHN5c3RlbSIgc3lzdGVtIGJ1bmRsZSBpbWFnZQ==:SU1BR0U=;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAllBMVEXe3t7b29vf398AAADf39/e3t7e3t7////Gxsbj4+OpqanFxcW5ubmtra2xsbGVlZWlpaWhoaHS0tLx8fG2trbU1NTBwcGvr6+MjIybm5twcHC+vr6jo6OYmJizs7Orq6vExMSZmZnCwsKdnZ2ampqRkZE3Nze7u7t+fn6EhIRjY2NVVVVsbGx5eXmJiYlEREQvLy8aGhprAy3xAAAABnRSTlPvIK8Av79l/pT7AAAJyElEQVR42uzVXW/aMBiGYWin57VjmyRmNGFLKOWjQACt/P8/t7yJQVMPrE62KkBcR88ZuokTD56HPwa4dU/D58Hw9jNabcYT7sIdHKve4F5C8Ai5No+Qa/MIuTaPkGvzCLk2QSHKIB6rFAIEhayniCc9fVx2c2ou++OUwi88ZGEtYrHbfZOf9/59r9w2f963Fj7hIUaOK0SSWyKSS8s7461rsOmk3TMNj/CQKVGBOJSiTsJNkpjcACjHxOYWHsEhCyJhEYWW1FkDeKNeDcD0c2LgERpi+McrxFFqIYQeaQCqnshWrQAUL7zFGh7BIVNq5YjDJm1Jnfd7lySrnQLT23aX8AgPWbjTG0VFTHKJyohlXLKZ8xy/wiMwxB3rsUEwd0yZ+ecdyQCs+yk1PAJDMuokiMFmxMZl/9W6PJ2EOlrBIzAkp84IUahym6yOG7D0WCarXQE2Oq6Scm3gERoyJxbrA5wJfiCV4l3zljVPu5R8IW7gERZy+fKPC0SgJu4e+XS0Ss+/FSkkI2eKQJ8vxOx7L8RXcjaI4W3GF2KhAKhU8CXYbTHjrZfwCAz5Tc4LohA/pVy7nbZRqds7IVYaHqEhc3ImFgGKkZM1TVO6XR4Oh9rtX+2uRo4BYodYQY7UCCDofwggdoiRdJYDuOEQYuFXYjEqk68qqwKIHZJHCoGQ9FVSgcUNSe8lxNBFBXarIVrSmQG71ZC/1Jpdc6sgEIbv3AXRIB9VE2OStmN6mpnT8/9/3RlSbGTxzmFC9yYTfV14wrIsRLQk/f4ikHDhM+ANVSD6BSB/VLg39SbDJKCyBxFst1o0tkEOsEP2ID20q/nXEFH2IK+Aai1tVVSUOYgrrnYrsx0VEQ2Zg3A6HVq/rlNRkzlI7U/jouMgutsyKmuQEulpHMYHdKW9X8kapIl//hHcIK2Isgbp4zWDRznr9C3KGuQAzkSQorSf60R0zhnEb9G1WF77K7AvYxETm0HYJ0sEUuko2V6lu/Zax6JhM8j+a58IZIjqkav13uuoaum3gtgJJkwDYqJ6ZARvZ0VFm0Nr4oC3NCBv3teBhJGzHS2I240g3MXVjScBEXTzcVm0S0WHbSBsYsAY+6fTgJDJLtOBdB2wzn0kD62yMYdew2z94dSIbaFFB2T/9Ql6YglA5kh6UxUf7X0cwNu9J/b1VCk5izaB7J1nt470t2TpVwup5pGwc/q1P0Q73ymzBYRPrqF7KzdMMEcYAHK79CpHDaDNUcHDEHFraE0ffLaPKUGJYizqwGldnAQKcyyuNrjO0YotIN3+YX2CEbno0OelKCofRgMLb/WiyLZolBYijqKZy5GakbudyhOkbHXM4Yt2UayQgCxzBMER1jiK2oOskZzb/EDqgvqTi7Kx8SJLRcfcQI60oWLwd16W5Qi+RCR5gYQcwYuSPKgiSxkNXE4glEO3D2cYvi6gBNXKfEAoB17oDv1loZaGkuQCQji0sG/0Pyu+BGFIHjjmASKDNrTr+VmRHmB4GoSchTM+B5CWLSisQOKrYvQwnvtiSy9n/PNBSvUYC/7hO4enWswgofNyd+Je33Hxw2KvzwYpR991I6wG9L44ADNVXYYgqhGSwY8I70/5b+fyuSBKAjDRCaPBmX4HZ8a3ik1dDt/OK7VrEVZEWvB3YQFG9VQQ+b+9c1tuGwTCcI+wIJCEhK26suW2aafpRfv+j9ehIgtZuQk20QRn8t9kRP7Y+5nDamUUDfbzr1AzWT2oDTQ65G+YQfiPYPqmBxD3TU0jh9tnHlo74CgGzHLRH5gKqUJ6xGBSTM6mLxwl1NMNrc1FPdLxexzsZq6iFONegCDBdJxPWgBNzvDgwrVnG5GqDavPBemkIBxM+ewHd4Rq/lEtTYREHLvn2njmJnJQB/Nq64sPtcWhFf34AswXjcd/JkvqrGcAoSey3xR+iauiX4sdjrBwTjwG00RIngGk2vJIdcUwRuMNNecCZDTC9pFJn/w0/kfSwjlqk0Eoh5Cdb67ji7o7AA4IEkwu+AFNgpCsKQpCxsQ0YLvDq9FkcdUCrtoKTTEtM1MGSS4Ijoj5KiJDWceFpmiyyz4Eacm2pzrq3WlVEgrSIQeM97deWayiPGzlffFkBm8Kqs3msRm/AgherIaBvOsco41NNk6I+46aUFVtgKNpJVGQnYOo5bB8xw5wZZWzyUfXyhF6nz0ATm986IysnWnHVhIF+dMcW92xpXxhC7EJws0KejoOf5xJeNMJdXq6GRqWqfydDyDIWRvAcgOdciZYcyrkg2hBzqMdmZOKTb0zFQ5CN2doDwJdbLoSkF5HDf2ySDBzzchWUz6IX3JjECzbqKlskNpvckA1WAJSk2HrKR/kK90c5IKmw61N3BxUGQVBzdmTSis4XApiKUjLvczS9JgU+X4rHQE/RMgDORAQMtwkmlJeDCVU6HhtHtbXLhPkBrNfFAztgGPiBwycT7XXPl7nJH9UQw4IjgZJyHzcxJQCgn+lRQQCKRe38kA0TePqBIjhTscUkPoUCKSA6DyQAUEiMjrcRm9KAWmn7SwAsFsvmSA7HjJAfIyGkDntqEmngPzu+cXqf2eA1HRL2ci9FF3JhhQQyTMkM0C2y4TopalpLBrEcqeJBO00UFN7BSBb0kA6AGZT0SDw34TYLkwlg3RAJzbQ/zCAaUCWDOJvBAVCFncAVr9QNIjAcjAiIx2gPEhVMIgWpBxUgl7qx+pXFA1CT7h1jyBVMF0NSK/jBhq38Q26YJA9TeMmAqGmkkFqWg7uA4imJlMwyFdaDtYcpWn1uy8YxD5w20WzMF0ByCFuoHFLNKWCbCRKcREdrAZyQ9O4XILg/SNtMogA1Ib30cFqIIqm8SNHHRamgoeWpuWg4ihLl+RjwSADBTEcJWn1q9LnyGfUrTtArQWCMZq4gcY9XlFCHEMDStEleSgYZEtXqImjDDWNySC9QPXxgVg9j0xx0DRue/byC59Qkm8+Ba0Oso3zCI0b7kwFDy2gSy1w1LQwlQuCFbqKG0gHAKyYEDfKKlB9Jkio0CEOmsQNAk1PDvJ9RpAiF0TgjtWogcStEKR6ahDZI1AeiBakHNQxSEfKeJEAouQZ+o7939+6Y3U5CP26T5N9xdiWBGL5eUKOsDtXZYL0OjTQuA0tfk8LnelC8DAk9WUge1rXNjGIWpoeVqXhLH2KmMBJmeoCkP1u90Pe6dfOCRtCGzG17FE1QogmeYrSpf0CkJZfoG0CCNb7yVUE/1JdN0jV4s3nhYIYlkiigHMx/mQ5IKa5QIYlvTBLVqUbxU7p9Rk9heoVpDS9gpSmV5DS9ApSml7Mg3XffGAvQu9ezkO037+9/sdov/n49v1fKIFpFaO3gooAAAAASUVORK5CYII=",
+ "scada": true,
+ "description": "Bundle with high-performance SCADA symbols for energy system",
+ "order": 9410,
+ "name": "High-performance SCADA energy system"
+ },
+ "widgetTypeFqns": [
+ "hp_solar_panel",
+ "hp_stand_solar_panel",
+ "hp_wind_turbine",
+ "hp_wind_turbine_cluster",
+ "hp_fuel_generator",
+ "hp_industrial_fuel_generator",
+ "hp_circuit_breaker2",
+ "hp_horizontal_circuit_breaker",
+ "hp_voltage_relay",
+ "hp_3_phase_voltage_relay",
+ "hp_voltage_stabilizer",
+ "hp_energy_meter",
+ "hp_two_rate_energy_meter",
+ "hp_three_rate_energy_meter",
+ "hp_four_rate_energy_meter",
+ "hp_electrical_distribution_board",
+ "hp_power_socket",
+ "hp_single_key_switch",
+ "hp_two_key_switch",
+ "hp_top_light_bulb",
+ "hp_bottom_light_bulb",
+ "hp_battery",
+ "hp_inverter",
+ "hp_large_inverter",
+ "hp_horizontal_energy_systems_controller",
+ "hp_vertical_energy_systems_controller",
+ "hp_small_power_transformer",
+ "hp_power_transformer",
+ "hp_low_voltage_transformer_tower",
+ "hp_low_voltage_tower",
+ "hp_high_voltage_tower",
+ "hp_consumers",
+ "hp_house",
+ "hp_apartments",
+ "hp_manufacture"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/update_device_attribute.json b/application/src/main/data/json/system/widget_types/update_device_attribute.json
index 92111daf54..763ba93bc9 100644
--- a/application/src/main/data/json/system/widget_types/update_device_attribute.json
+++ b/application/src/main/data/json/system/widget_types/update_device_attribute.json
@@ -9,7 +9,7 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
- "templateHtml": "
",
+ "templateHtml": "",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n",
"settingsSchema": "",
diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json
index bce88d62b0..0f2473cde6 100644
--- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json
+++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json
@@ -19,8 +19,13 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
+ "configurationVersion": 1,
"configuration": {
- "defaultTTL": 0
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
}
},
{
@@ -134,4 +139,4 @@
],
"ruleChainConnections": null
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
index ee38849c1b..8efda98c5b 100644
--- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
+++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
@@ -18,8 +18,13 @@
},
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
+ "configurationVersion": 1,
"configuration": {
- "defaultTTL": 0
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
}
},
{
diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql
index 64bbbaca05..eccc510f42 100644
--- a/application/src/main/data/upgrade/basic/schema_update.sql
+++ b/application/src/main/data/upgrade/basic/schema_update.sql
@@ -14,198 +14,50 @@
-- limitations under the License.
--
-ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS last_login_ts BIGINT;
-UPDATE user_credentials c SET last_login_ts = (SELECT (additional_info::json ->> 'lastLoginTs')::bigint FROM tb_user u WHERE u.id = c.user_id)
- WHERE last_login_ts IS NULL;
+-- UPDATE SAVE TIME SERIES NODES START
-ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS failed_login_attempts INT;
-UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::json ->> 'failedLoginAttempts')::int FROM tb_user u WHERE u.id = c.user_id)
- WHERE failed_login_attempts IS NULL;
-
-UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
- WHERE additional_info IS NOT NULL AND additional_info != 'null' AND jsonb_typeof(additional_info::jsonb) = 'object';
-
--- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START
-
-ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS debug_settings varchar(1024) DEFAULT null;
-DO
-$$
- BEGIN
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'rule_node' AND column_name = 'debug_mode')
- THEN
- UPDATE rule_node SET debug_settings = '{"failuresEnabled": true, "allEnabledUntil": ' || cast((extract(epoch from now()) + 900) * 1000 as bigint) || '}' WHERE debug_mode = true; -- 15 minutes according to thingsboard.yml default settings.
- ALTER TABLE rule_node DROP COLUMN debug_mode;
- END IF;
- END
-$$;
-
--- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY END
-
-
--- CREATE MOBILE APP BUNDLES FROM EXISTING APPS
-
-CREATE TABLE IF NOT EXISTS mobile_app_bundle (
- id uuid NOT NULL CONSTRAINT mobile_app_bundle_pkey PRIMARY KEY,
- created_time bigint NOT NULL,
- tenant_id uuid,
- title varchar(255),
- description varchar(1024),
- android_app_id uuid UNIQUE,
- ios_app_id uuid UNIQUE,
- layout_config varchar(16384),
- oauth2_enabled boolean,
- CONSTRAINT fk_android_app_id FOREIGN KEY (android_app_id) REFERENCES mobile_app(id) ON DELETE SET NULL,
- CONSTRAINT fk_ios_app_id FOREIGN KEY (ios_app_id) REFERENCES mobile_app(id) ON DELETE SET NULL
-);
-CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id);
-
-ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS platform_type varchar(32),
- ADD COLUMN IF NOT EXISTS status varchar(32),
- ADD COLUMN IF NOT EXISTS version_info varchar(100000),
- ADD COLUMN IF NOT EXISTS store_info varchar(16384),
- DROP CONSTRAINT IF EXISTS mobile_app_pkg_name_key,
- DROP CONSTRAINT IF EXISTS mobile_app_unq_key;
-
--- rename mobile_app_oauth2_client to mobile_app_bundle_oauth2_client
-DO
-$$
- BEGIN
- -- in case of running the upgrade script a second time
- IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'mobile_app_oauth2_client') THEN
- ALTER TABLE mobile_app_oauth2_client RENAME TO mobile_app_bundle_oauth2_client;
- ALTER TABLE mobile_app_bundle_oauth2_client DROP CONSTRAINT IF EXISTS fk_domain;
- ALTER TABLE mobile_app_bundle_oauth2_client RENAME COLUMN mobile_app_id TO mobile_app_bundle_id;
- END IF;
- END;
-$$;
-
-
-CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-
--- duplicate each mobile app and create mobile app bundle for the pair of android and ios app
-DO
-$$
- DECLARE
- generatedBundleId uuid;
- iosAppId uuid;
- mobileAppRecord RECORD;
+DO $$
BEGIN
- -- in case of running the upgrade script a second time
- IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'mobile_app' and column_name = 'oauth2_enabled') THEN
- UPDATE mobile_app SET platform_type = 'ANDROID' WHERE platform_type IS NULL;
- UPDATE mobile_app SET status = 'DRAFT' WHERE mobile_app.status IS NULL;
- FOR mobileAppRecord IN SELECT * FROM mobile_app
- LOOP
- -- duplicate app for iOS platform type
- iosAppId := uuid_generate_v4();
- INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, app_secret, platform_type, status)
- VALUES (iosAppId, mobileAppRecord.created_time, mobileAppRecord.tenant_id, mobileAppRecord.pkg_name, mobileAppRecord.app_secret, 'IOS', mobileAppRecord.status)
- ON CONFLICT DO NOTHING;
- -- create bundle for android and iOS app
- generatedBundleId := uuid_generate_v4();
- INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id, ios_app_id, oauth2_enabled)
- VALUES (generatedBundleId, mobileAppRecord.created_time, mobileAppRecord.tenant_id,
- mobileAppRecord.pkg_name || ' (autogenerated)', mobileAppRecord.id, iosAppId, mobileAppRecord.oauth2_enabled)
- ON CONFLICT DO NOTHING;
- UPDATE mobile_app_bundle_oauth2_client SET mobile_app_bundle_id = generatedBundleId WHERE mobile_app_bundle_id = mobileAppRecord.id;
- END LOOP;
- END IF;
- IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'fk_mobile_app_bundle_oauth2_client_bundle_id') THEN
- ALTER TABLE mobile_app_bundle_oauth2_client ADD CONSTRAINT fk_mobile_app_bundle_oauth2_client_bundle_id
- FOREIGN KEY (mobile_app_bundle_id) REFERENCES mobile_app_bundle(id) ON DELETE CASCADE;
- END IF;
- ALTER TABLE mobile_app DROP COLUMN IF EXISTS oauth2_enabled;
- IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'mobile_app_pkg_name_platform_unq_key') THEN
- ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_pkg_name_platform_unq_key UNIQUE (pkg_name, platform_type);
- END IF;
- END;
-$$;
+ -- Check if the rule_node table exists
+ IF EXISTS (
+ SELECT 1
+ FROM information_schema.tables
+ WHERE table_name = 'rule_node'
+ ) THEN
+
+ UPDATE rule_node
+ SET configuration = (
+ (configuration::jsonb - 'skipLatestPersistence')
+ || jsonb_build_object(
+ 'processingSettings', jsonb_build_object(
+ 'type', 'ADVANCED',
+ 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'),
+ 'latest', jsonb_build_object('type', 'SKIP'),
+ 'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE')
+ )
+ )
+ )::text,
+ configuration_version = 1
+ WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
+ AND configuration_version = 0
+ AND configuration::jsonb ->> 'skipLatestPersistence' = 'true';
+
+ UPDATE rule_node
+ SET configuration = (
+ (configuration::jsonb - 'skipLatestPersistence')
+ || jsonb_build_object(
+ 'processingSettings', jsonb_build_object(
+ 'type', 'ON_EVERY_MESSAGE'
+ )
+ )
+ )::text,
+ configuration_version = 1
+ WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode'
+ AND configuration_version = 0
+ AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL);
-ALTER TABLE IF EXISTS mobile_app_settings RENAME TO qr_code_settings;
-ALTER TABLE qr_code_settings ADD COLUMN IF NOT EXISTS mobile_app_bundle_id uuid,
- ADD COLUMN IF NOT EXISTS android_enabled boolean,
- ADD COLUMN IF NOT EXISTS ios_enabled boolean;
-
--- migrate mobile apps from qr code settings to mobile_app, create mobile app bundle for the pair of apps
-DO
-$$
- DECLARE
- androidPkgName varchar;
- iosPkgName varchar;
- androidAppId uuid;
- iosAppId uuid;
- generatedBundleId uuid;
- qrCodeRecord RECORD;
- BEGIN
- -- in case of running the upgrade script a second time
- IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'qr_code_settings' AND column_name = 'android_config') THEN
- FOR qrCodeRecord IN SELECT * FROM qr_code_settings
- LOOP
- generatedBundleId := NULL;
- -- migrate android config
- IF (qrCodeRecord.android_config::jsonb ->> 'appPackage' IS NOT NULL) THEN
- androidPkgName := qrCodeRecord.android_config::jsonb ->> 'appPackage';
- SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID';
- IF androidAppId IS NULL THEN
- androidAppId := uuid_generate_v4();
- INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info)
- VALUES (androidAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id,
- androidPkgName, 'ANDROID', 'DRAFT', qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled');
- generatedBundleId := uuid_generate_v4();
- INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id)
- VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId);
- UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
- ELSE
- UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled' WHERE id = androidAppId;
- UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId);
- END IF;
- END IF;
- UPDATE qr_code_settings SET android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
-
- -- migrate ios config
- IF (qrCodeRecord.ios_config::jsonb ->> 'appId' IS NOT NULL) THEN
- iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1);
- SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS';
- IF iosAppId IS NULL THEN
- iosAppId := uuid_generate_v4();
- INSERT INTO mobile_app(id, created_time, tenant_id, pkg_name, platform_type, status, store_info)
- VALUES (iosAppId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id,
- iosPkgName, 'IOS', 'DRAFT', qrCodeRecord.ios_config::jsonb - 'enabled');
- IF generatedBundleId IS NULL THEN
- generatedBundleId := uuid_generate_v4();
- INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id)
- VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId);
- UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
- ELSE
- UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId;
- END IF;
- ELSE
- UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId);
- UPDATE mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId;
- END IF;
- END IF;
- UPDATE qr_code_settings SET ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id;
- END LOOP;
- ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key;
- ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_pkey TO qr_code_settings_pkey;
- END IF;
- ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config;
- END;
-$$;
-
--- update constraint name
-DO
-$$
- BEGIN
- ALTER TABLE domain DROP CONSTRAINT IF EXISTS domain_unq_key;
- IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'domain_name_key') THEN
- ALTER TABLE domain ADD CONSTRAINT domain_name_key UNIQUE (name);
END IF;
END;
$$;
--- UPDATE RESOURCE JS_MODULE SUB TYPE START
-
-UPDATE resource SET resource_sub_type = 'EXTENSION' WHERE resource_type = 'JS_MODULE' AND resource_sub_type IS NULL;
-
--- UPDATE RESOURCE JS_MODULE SUB TYPE END
+-- UPDATE SAVE TIME SERIES NODES END
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 3dca3417b4..77bf7c32fa 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -865,6 +865,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) {
+ attributeSubscriptions.remove(sessionId);
+ rpcSubscriptions.remove(sessionId);
notifyTransportAboutClosedSession(sessionId, sessionMd, TransportSessionCloseReason.MAX_CONCURRENT_SESSIONS_LIMIT_REACHED);
}
diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
index c32533bd04..0f3f529cb7 100644
--- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
@@ -84,6 +84,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
public class SwaggerConfiguration {
public static final String LOGIN_ENDPOINT = "/api/auth/login";
+ public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
@@ -150,6 +151,7 @@ public class SwaggerConfiguration {
.info(info);
addDefaultSchemas(openApi);
addLoginOperation(openApi);
+ addRefreshTokenOperation(openApi);
return openApi;
}
@@ -210,6 +212,29 @@ public class SwaggerConfiguration {
openAPI.path(LOGIN_ENDPOINT, pathItem);
}
+ private void addRefreshTokenOperation(OpenAPI openAPI) {
+ var operation = new Operation();
+ operation.summary("Refresh user JWT token data");
+ operation.description("""
+ Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
+
+ The response contains a new token that can be used for authorization.
+
+ `X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
+
+ var requestBody = new RequestBody().description("Refresh token request")
+ .content(new Content().addMediaType(APPLICATION_JSON_VALUE,
+ new MediaType().schema(new Schema().addProperty("refreshToken", new Schema<>().type("string")))));
+
+ operation.requestBody(requestBody);
+
+ operation.responses(loginResponses);
+
+ operation.addTagsItem("login-endpoint");
+ var pathItem = new PathItem().post(operation);
+ openAPI.path(REFRESH_TOKEN_ENDPOINT, pathItem);
+ }
+
@Bean
public GroupedOpenApi groupedApi(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return GroupedOpenApi.builder()
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index 622c0ab2dc..ea4c059222 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -35,8 +35,6 @@ import org.thingsboard.server.service.install.migrate.TsLatestMigrateService;
import org.thingsboard.server.service.install.update.CacheCleanupService;
import org.thingsboard.server.service.install.update.DataUpdateService;
-import static org.thingsboard.server.service.install.update.DefaultDataUpdateService.getEnv;
-
@Service
@Profile("install")
@Slf4j
@@ -99,8 +97,6 @@ public class ThingsboardInstallService {
if ("cassandra-latest-to-postgres".equals(upgradeFromVersion)) {
log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ...");
latestMigrateService.migrate();
- } else if (upgradeFromVersion.equals("3.9.0-resources")) {
- installScripts.updateResourcesUsage();
} else {
// TODO DON'T FORGET to update SUPPORTED_VERSIONS_FROM in DefaultDatabaseSchemaSettingsService
databaseSchemaVersionService.validateSchemaSettings();
@@ -118,25 +114,16 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
// Creates missing indexes.
entityDatabaseSchemaService.createDatabaseIndexes();
- // Runs upgrade scripts that are not possible in plain SQL.
+
// TODO: cleanup update code after each release
- if (!getEnv("SKIP_RESOURCES_USAGE_MIGRATION", false)) {
- installScripts.setUpdateResourcesUsage(true);
- } else {
- log.info("Skipping resources usage migration. Run the upgrade with fromVersion as '3.9.0-resources' to migrate");
- }
- if (installScripts.isUpdateResourcesUsage()) {
- installScripts.updateResourcesUsage();
- }
+
+ // Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImagesAndResources();
- if (installScripts.isUpdateImages()) {
- installScripts.updateImages();
- }
databaseSchemaVersionService.updateSchemaVersion();
}
log.info("Upgrade finished successfully!");
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
index 52277b8527..faf001fbba 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
+import java.util.HashSet;
import java.util.Set;
@Slf4j
@@ -40,54 +41,63 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
if (dashboard == null) {
throw new RuntimeException("[{" + tenantId + "}] dashboardUpdateMsg {" + dashboardUpdateMsg + "} cannot be converted to dashboard");
}
- Set assignedCustomers = null;
+ Set newAssignedCustomers = new HashSet<>(dashboard.getAssignedCustomers());
Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId);
if (dashboardById == null) {
created = true;
dashboard.setId(null);
+ dashboard.setAssignedCustomers(null);
} else {
dashboard.setId(dashboardId);
- assignedCustomers = filterNonExistingCustomers(tenantId, dashboardById.getAssignedCustomers());
+ dashboard.setAssignedCustomers(dashboardById.getAssignedCustomers());
}
dashboardValidator.validate(dashboard, Dashboard::getTenantId);
if (created) {
dashboard.setId(dashboardId);
}
- Set msgAssignedCustomers = filterNonExistingCustomers(tenantId, dashboard.getAssignedCustomers());
- if (msgAssignedCustomers != null) {
- if (assignedCustomers == null) {
- assignedCustomers = msgAssignedCustomers;
- } else {
- assignedCustomers.addAll(msgAssignedCustomers);
+
+ Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false);
+
+ updateDashboardAssignments(tenantId, dashboardById, savedDashboard, newAssignedCustomers);
+
+ return created;
+ }
+
+ private void updateDashboardAssignments(TenantId tenantId, Dashboard dashboardById, Dashboard savedDashboard, Set newAssignedCustomers) {
+ Set currentAssignedCustomers = new HashSet<>();
+ if (dashboardById != null) {
+ if (dashboardById.getAssignedCustomers() != null) {
+ currentAssignedCustomers.addAll(dashboardById.getAssignedCustomers());
}
}
- dashboard.setAssignedCustomers(assignedCustomers);
- Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false);
- if (msgAssignedCustomers != null && !msgAssignedCustomers.isEmpty()) {
- for (ShortCustomerInfo assignedCustomer : msgAssignedCustomers) {
- if (assignedCustomer.getCustomerId().equals(customerId)) {
- edgeCtx.getDashboardService().assignDashboardToCustomer(tenantId, savedDashboard.getId(), assignedCustomer.getCustomerId());
- }
+
+ newAssignedCustomers = filterNonExistingCustomers(tenantId, currentAssignedCustomers, newAssignedCustomers);
+
+ Set addedCustomerIds = new HashSet<>();
+ Set removedCustomerIds = new HashSet<>();
+ for (ShortCustomerInfo newAssignedCustomer : newAssignedCustomers) {
+ if (!savedDashboard.isAssignedToCustomer(newAssignedCustomer.getCustomerId())) {
+ addedCustomerIds.add(newAssignedCustomer.getCustomerId());
}
- } else {
- unassignCustomersFromDashboard(tenantId, savedDashboard, customerId);
}
- return created;
- }
- private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard, CustomerId customerId) {
- if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) {
- for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) {
- if (assignedCustomer.getCustomerId().equals(customerId)) {
- edgeCtx.getDashboardService().unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId());
- }
+ for (ShortCustomerInfo currentAssignedCustomer : currentAssignedCustomers) {
+ if (!newAssignedCustomers.contains(currentAssignedCustomer)) {
+ removedCustomerIds.add(currentAssignedCustomer.getCustomerId());
}
}
+
+ for (CustomerId customerIdToAdd : addedCustomerIds) {
+ edgeCtx.getDashboardService().assignDashboardToCustomer(tenantId, savedDashboard.getId(), customerIdToAdd);
+ }
+ for (CustomerId customerIdToRemove : removedCustomerIds) {
+ edgeCtx.getDashboardService().unassignDashboardFromCustomer(tenantId, savedDashboard.getId(), customerIdToRemove);
+ }
}
protected abstract Dashboard constructDashboardFromUpdateMsg(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg);
- protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers);
+ protected abstract Set filterNonExistingCustomers(TenantId tenantId, Set currentAssignedCustomers, Set newAssignedCustomers);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
index cb54977c3c..8d953907b6 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
@@ -128,9 +128,9 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl
}
@Override
- protected Set filterNonExistingCustomers(TenantId tenantId, Set assignedCustomers) {
- // do nothing on cloud
- return assignedCustomers;
+ protected Set filterNonExistingCustomers(TenantId tenantId, Set currentAssignedCustomers, Set newAssignedCustomers) {
+ newAssignedCustomers.addAll(currentAssignedCustomers);
+ return newAssignedCustomers;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessorV1.java
index 20ab6019bf..c13c85779a 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessorV1.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessorV1.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import java.util.HashSet;
import java.util.Set;
@Component
@@ -44,7 +45,7 @@ public class DashboardEdgeProcessorV1 extends DashboardEdgeProcessor {
Set assignedCustomers;
if (dashboardUpdateMsg.hasAssignedCustomers()) {
assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {});
- assignedCustomers = filterNonExistingCustomers(tenantId, assignedCustomers);
+ assignedCustomers = filterNonExistingCustomers(tenantId, new HashSet<>(), assignedCustomers);
dashboard.setAssignedCustomers(assignedCustomers);
}
dashboard.setMobileOrder(dashboardUpdateMsg.hasMobileOrder() ? dashboardUpdateMsg.getMobileOrder() : null);
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
index ac7bd6768c..a9a3bcf9fb 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
@@ -53,6 +53,9 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
}
String resourceKey = resource.getResourceKey();
ResourceType resourceType = resource.getResourceType();
+ if (!created && !resourceType.isUpdatable()) {
+ resource.setData(null);
+ }
PageDataIterable resourcesIterable = new PageDataIterable<>(
link -> edgeCtx.getResourceService().findTenantResourcesByResourceTypeAndPageLink(tenantId, resourceType, link), 1024);
for (TbResource tbResource : resourcesIterable) {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
index 21eec71388..2adb6822dc 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.entitiy.dashboard;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
@@ -30,6 +31,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.install.ProjectInfo;
import org.thingsboard.server.service.sync.GitSyncService;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
@@ -51,10 +53,11 @@ public class DashboardSyncService {
private final ImageService imageService;
private final WidgetsBundleService widgetsBundleService;
private final PartitionService partitionService;
+ private final ProjectInfo projectInfo;
@Value("${transport.gateway.dashboard.sync.repository_url:}")
private String repoUrl;
- @Value("${transport.gateway.dashboard.sync.branch:main}")
+ @Value("${transport.gateway.dashboard.sync.branch:}")
private String branch;
@Value("${transport.gateway.dashboard.sync.fetch_frequency:24}")
private int fetchFrequencyHours;
@@ -64,6 +67,9 @@ public class DashboardSyncService {
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() throws Exception {
+ if (StringUtils.isBlank(branch)) {
+ branch = "release/" + projectInfo.getProjectVersion();
+ }
gitSyncService.registerSync(REPO_KEY, repoUrl, branch, TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
index 686e85cb1e..2c384aa493 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
@@ -348,7 +348,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
.tenantId(entityView.getTenantId())
.entityId(entityId)
.entries(latestValues)
- .onlyLatest(true)
+ .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.callback(new FutureCallback() {
@Override
public void onSuccess(@Nullable Void tmp) {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
index f1d9259e91..3b21685d22 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
@@ -17,9 +17,6 @@ package org.thingsboard.server.service.install;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@@ -33,37 +30,29 @@ import java.util.List;
@RequiredArgsConstructor
public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSettingsService {
- private static final String CURRENT_PRODUCT = "CE";
// This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
- private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.8.0", "3.8.1");
+ private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.9.0");
- private final BuildProperties buildProperties;
+ private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate;
- @Value("${install.upgrade.from_version:}")
- private String upgradeFromVersion;
-
private String packageSchemaVersion;
private String schemaVersionFromDb;
@Override
public void validateSchemaSettings() {
- //TODO: remove after release (3.9.0)
- createProductIfNotExists();
-
- String dbSchemaVersion = getDbSchemaVersion();
-
if (DefaultDataUpdateService.getEnv("SKIP_SCHEMA_VERSION_CHECK", false)) {
log.info("Skipped DB schema version check due to SKIP_SCHEMA_VERSION_CHECK set to 'true'.");
return;
}
String product = getProductFromDb();
- if (!CURRENT_PRODUCT.equals(product)) {
- onSchemaSettingsError(String.format("Upgrade failed: can't upgrade ThingsBoard %s database using ThingsBoard %s.", product, CURRENT_PRODUCT));
+ if (!projectInfo.getProductType().equals(product)) {
+ onSchemaSettingsError(String.format("Upgrade failed: can't upgrade ThingsBoard %s database using ThingsBoard %s.", product, projectInfo.getProductType()));
}
+ String dbSchemaVersion = getDbSchemaVersion();
if (dbSchemaVersion.equals(getPackageSchemaVersion())) {
onSchemaSettingsError("Upgrade failed: database already upgraded to current version. You can set SKIP_SCHEMA_VERSION_CHECK to 'true' if force re-upgrade needed.");
}
@@ -75,19 +64,11 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
}
}
- @Deprecated(forRemoval = true, since = "3.9.0")
- private void createProductIfNotExists() {
- boolean isCommunityEdition = jdbcTemplate.queryForList(
- "SELECT 1 FROM information_schema.tables WHERE table_name = 'integration'", Integer.class).isEmpty();
- String product = isCommunityEdition ? "CE" : "PE";
- jdbcTemplate.execute("ALTER TABLE tb_schema_settings ADD COLUMN IF NOT EXISTS product varchar(2) DEFAULT '" + product + "'");
- }
-
@Override
public void createSchemaSettings() {
Long schemaVersion = getSchemaVersionFromDb();
if (schemaVersion == null) {
- jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + getPackageSchemaVersionForDb() + ", '" + CURRENT_PRODUCT + "')");
+ jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + getPackageSchemaVersionForDb() + ", '" + projectInfo.getProductType() + "')");
}
}
@@ -99,7 +80,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
@Override
public String getPackageSchemaVersion() {
if (packageSchemaVersion == null) {
- packageSchemaVersion = buildProperties.getVersion().replaceAll("[^\\d.]", "");
+ packageSchemaVersion = projectInfo.getProjectVersion();
}
return packageSchemaVersion;
}
@@ -107,15 +88,6 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
@Override
public String getDbSchemaVersion() {
if (schemaVersionFromDb == null) {
- if (StringUtils.isNotBlank(upgradeFromVersion)) {
- /*
- * TODO - Remove after the release of 3.9.0:
- * This a temporary workaround due to the issue that schema version in the
- * tb_schema_settings was set as 3.6.4 during the install of 3.8.1.
- * */
- schemaVersionFromDb = upgradeFromVersion;
- return schemaVersionFromDb;
- }
Long version = getSchemaVersionFromDb();
if (version == null) {
onSchemaSettingsError("Upgrade failed: the database schema version is missing.");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
index a6ccb85e11..b230be0d57 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.Getter;
-import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -120,11 +118,6 @@ public class InstallScripts {
@Autowired
private ResourcesUpdater resourcesUpdater;
- @Getter @Setter
- private boolean updateImages = false;
-
- @Getter @Setter
- private boolean updateResourcesUsage = false;
@Autowired
private ImageService imageService;
@@ -395,14 +388,6 @@ public class InstallScripts {
}
}
- public void updateImages() {
- resourcesUpdater.updateWidgetsBundlesImages();
- resourcesUpdater.updateWidgetTypesImages();
- resourcesUpdater.updateDashboardsImages();
- resourcesUpdater.updateDeviceProfilesImages();
- resourcesUpdater.updateAssetProfilesImages();
- }
-
public void loadSystemImagesAndResources() {
log.info("Loading system images and resources...");
Stream dashboardsFiles = Stream.concat(listDir(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
@@ -512,11 +497,6 @@ public class InstallScripts {
}
}
- public void updateResourcesUsage() {
- resourcesUpdater.updateDashboardsResources();
- resourcesUpdater.updateWidgetsResources();
- }
-
private void loadSystemResources(Path dir, ResourceType resourceType, ResourceSubType resourceSubType) {
listDir(dir).forEach(resourceFile -> {
String resourceKey = resourceFile.getFileName().toString();
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.scss b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java
similarity index 56%
rename from ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.scss
rename to application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java
index da530c0540..eddd4c8ccf 100644
--- a/ui-ngx/src/app/modules/home/components/rule-node/common/example-hint.component.scss
+++ b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java
@@ -13,19 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-:host {
- .space-between {
- display: flex;
- justify-content: space-between;
- gap: 20px;
+package org.thingsboard.server.service.install;
- .see-example {
- display: flex;
- flex-shrink: 0;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ProjectInfo {
+
+ private final BuildProperties buildProperties;
+
+ public String getProjectVersion() {
+ return buildProperties.getVersion().replaceAll("[^\\d.]", "");
+ }
+
+ public String getProductType() {
+ return "CE";
}
- }
- .hint-text {
- width: 100%;
- }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index e8622fae37..cea938bce9 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
@@ -34,7 +34,6 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
-import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
@@ -58,9 +57,6 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
JpaExecutorService jpaExecutorService;
- @Autowired
- private InstallScripts installScripts;
-
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
index debc1cb987..27a64dc443 100644
--- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
@@ -115,8 +115,9 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
ToDeviceRpcRequest request = new ToDeviceRpcRequest(src.getRequestUUID(), src.getTenantId(), src.getDeviceId(),
src.isOneway(), src.getExpirationTime(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()), src.isPersisted(), src.getRetries(), src.getAdditionalInfo());
forwardRpcRequestToDeviceActor(request, response -> {
- if (src.isRestApiCall()) {
- sendRpcResponseToTbCore(src.getOriginServiceId(), response);
+ String originServiceId = src.getOriginServiceId();
+ if (src.isRestApiCall() && originServiceId != null) {
+ sendRpcResponseToTbCore(originServiceId, response);
}
consumer.accept(RuleEngineDeviceRpcResponse.builder()
.deviceId(src.getDeviceId())
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index d4444d5d7f..cab2f18dc7 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -118,10 +118,10 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
EntityId entityId = request.getEntityId();
checkInternalEntity(entityId);
boolean sysTenant = TenantId.SYS_TENANT_ID.equals(tenantId) || tenantId == null;
- if (sysTenant || request.isOnlyLatest() || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) {
+ if (sysTenant || !request.getStrategy().saveTimeseries() || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) {
KvUtils.validate(request.getEntries(), valueNoXssValidation);
ListenableFuture future = saveTimeseriesInternal(request);
- if (!request.isOnlyLatest()) {
+ if (request.getStrategy().saveTimeseries()) {
FutureCallback callback = getApiUsageCallback(tenantId, request.getCustomerId(), sysTenant, request.getCallback());
Futures.addCallback(future, callback, tsCallBackExecutor);
}
@@ -134,19 +134,24 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
public ListenableFuture saveTimeseriesInternal(TimeseriesSaveRequest request) {
TenantId tenantId = request.getTenantId();
EntityId entityId = request.getEntityId();
+ TimeseriesSaveRequest.Strategy strategy = request.getStrategy();
ListenableFuture saveFuture;
- if (request.isOnlyLatest()) {
- saveFuture = Futures.transform(tsService.saveLatest(tenantId, entityId, request.getEntries()), result -> 0, MoreExecutors.directExecutor());
- } else if (request.isSaveLatest()) {
+ if (strategy.saveTimeseries() && strategy.saveLatest()) {
saveFuture = tsService.save(tenantId, entityId, request.getEntries(), request.getTtl());
- } else {
+ } else if (strategy.saveLatest()) {
+ saveFuture = Futures.transform(tsService.saveLatest(tenantId, entityId, request.getEntries()), result -> 0, MoreExecutors.directExecutor());
+ } else if (strategy.saveTimeseries()) {
saveFuture = tsService.saveWithoutLatest(tenantId, entityId, request.getEntries(), request.getTtl());
+ } else {
+ saveFuture = Futures.immediateFuture(0);
}
addMainCallback(saveFuture, request.getCallback());
- addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, request.getEntries()));
- if (request.isSaveLatest() && !request.isOnlyLatest()) {
- addEntityViewCallback(tenantId, entityId, request.getEntries());
+ if (strategy.sendWsUpdate()) {
+ addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, request.getEntries()));
+ }
+ if (strategy.saveLatest()) {
+ copyLatestToEntityViews(tenantId, entityId, request.getEntries());
}
return saveFuture;
}
@@ -201,7 +206,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
}
- private void addEntityViewCallback(TenantId tenantId, EntityId entityId, List ts) {
+ private void copyLatestToEntityViews(TenantId tenantId, EntityId entityId, List ts) {
if (EntityType.DEVICE.equals(entityId.getEntityType()) || EntityType.ASSET.equals(entityId.getEntityType())) {
Futures.addCallback(this.tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId),
new FutureCallback<>() {
@@ -232,7 +237,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
.tenantId(tenantId)
.entityId(entityView.getId())
.entries(entityViewLatest)
- .onlyLatest(true)
+ .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
.callback(new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void tmp) {}
diff --git a/application/src/main/java/org/thingsboard/server/service/update/DeprecationService.java b/application/src/main/java/org/thingsboard/server/service/update/DeprecationService.java
deleted file mode 100644
index ff1f013953..0000000000
--- a/application/src/main/java/org/thingsboard/server/service/update/DeprecationService.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Copyright © 2016-2024 The Thingsboard Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.thingsboard.server.service.update;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-import org.thingsboard.rule.engine.api.NotificationCenter;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.info.GeneralNotificationInfo;
-import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
-import org.thingsboard.server.dao.notification.DefaultNotifications;
-import org.thingsboard.server.dao.notification.DefaultNotifications.DefaultNotification;
-import org.thingsboard.server.queue.util.AfterStartUp;
-
-import java.util.Map;
-
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class DeprecationService {
-
- private final NotificationCenter notificationCenter;
-
- @Value("${queue.type}")
- private String queueType;
-
- @Value("${database.ts.type}")
- private String tsType;
-
- @Value("${database.ts_latest.type}")
- private String tsLatestType;
-
- @AfterStartUp(order = Integer.MAX_VALUE)
- public void checkDeprecation() {
- checkQueueTypeDeprecation();
- checkDatabaseTypeDeprecation();
- }
-
- private void checkQueueTypeDeprecation() {
- String queueTypeName;
- switch (queueType) {
- case "aws-sqs" -> queueTypeName = "AWS SQS";
- case "pubsub" -> queueTypeName = "PubSub";
- case "service-bus" -> queueTypeName = "Azure Service Bus";
- case "rabbitmq" -> queueTypeName = "RabbitMQ";
- default -> {
- return;
- }
- }
-
- log.warn("WARNING: Starting with ThingsBoard 4.0, {} will no longer be supported as a message queue for microservices. " +
- "Please migrate to Apache Kafka. This change will not impact any rule nodes", queueTypeName);
- sendNotification(DefaultNotifications.queueTypeDeprecation, Map.of(
- "queueType", queueTypeName
- ));
- }
-
- private void checkDatabaseTypeDeprecation() {
- String deprecatedDatabaseType = "Timescale";
- if (StringUtils.equalsAnyIgnoreCase(deprecatedDatabaseType, tsType, tsLatestType)) {
- log.warn("WARNING: Starting with ThingsBoard 4.0, {} will no longer be supported as a storage provider. " +
- "Please migrate to Cassandra or PostgreSQL.", deprecatedDatabaseType);
- sendNotification(DefaultNotifications.databaseTypeDeprecation, Map.of(
- "databaseType", deprecatedDatabaseType
- ));
- }
- }
-
- private void sendNotification(DefaultNotification notification, Map info) {
- notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(),
- notification.toTemplate(), new GeneralNotificationInfo(info));
- }
-
-}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index e41d0c2aed..c4483f6273 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -1271,7 +1271,7 @@ transport:
# URL of gateways dashboard repository
repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}"
# Branch of gateways dashboard repository to work with
- branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:main}"
+ branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:}"
# Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
index 3b252046ab..5b09e05a09 100644
--- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.edge;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.Sets;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
@@ -175,7 +176,11 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
@Test
public void testSendDashboardToCloud() throws Exception {
- Dashboard dashboard = buildDashboardForUplinkMsg();
+ Customer customer = new Customer();
+ customer.setTitle("Edge Customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder();
@@ -196,6 +201,11 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Dashboard foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals("Edge Test Dashboard", foundDashboard.getName());
+
+ PageData pageData = doGetTypedWithPageLink("/api/customer/" + savedCustomer.getId().toString() + "/dashboards?",
+ new TypeReference<>() {}, new PageLink(100));
+ Assert.assertEquals(1, pageData.getData().size());
+ Assert.assertEquals("Edge Test Dashboard", pageData.getData().get(0).getTitle());
}
@Test
@@ -242,11 +252,12 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
return savedDashboard;
}
- private Dashboard buildDashboardForUplinkMsg() {
+ private Dashboard buildDashboardForUplinkMsg(Customer savedCustomer) {
Dashboard dashboard = new Dashboard();
dashboard.setId(new DashboardId(UUID.randomUUID()));
dashboard.setTenantId(tenantId);
dashboard.setTitle("Edge Test Dashboard");
+ dashboard.setAssignedCustomers(Sets.newHashSet(new ShortCustomerInfo(savedCustomer.getId(), savedCustomer.getTitle(), savedCustomer.isPublic())));
return dashboard;
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
index c27489d207..cc5aa4da0e 100644
--- a/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
@@ -98,30 +99,23 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
public void testSendResourceToCloud() throws Exception {
TbResource tbResource = createTbResource();
UUID uuid = Uuids.timeBased();
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, tbResource, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
- UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
- ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder();
- resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
- resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
- resourceUpdateMsgBuilder.setEntity(JacksonUtil.toString(tbResource));
- resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
- testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder);
- uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build());
+ checkResourceOnCloud(uplinkMsg, uuid, tbResource.getTitle());
+ }
- testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+ @Test
+ public void testUpdateResourceTitleOnCloud() throws Exception {
+ TbResource tbResource = createTbResource();
+ UUID uuid = Uuids.timeBased();
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, tbResource, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
- edgeImitator.expectResponsesAmount(1);
- edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+ checkResourceOnCloud(uplinkMsg, uuid, tbResource.getTitle());
- Assert.assertTrue(edgeImitator.waitForResponses());
+ tbResource.setTitle("Updated Edge Test Resource");
+ UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, tbResource, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
- UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
- Assert.assertTrue(latestResponseMsg.getSuccess());
-
- TbResource tb = doGet("/api/resource/" + uuid, TbResource.class);
- Assert.assertNotNull(tb);
- Assert.assertEquals("Edge Test Resource", tb.getName());
- Assert.assertEquals(TEST_DATA, tb.getEncodedData());
+ checkResourceOnCloud(updatedUplinkMsg, uuid, tbResource.getTitle());
}
@Test
@@ -134,21 +128,12 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
UUID uuid = Uuids.timeBased();
- UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
- ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder();
- resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
- resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
- resourceUpdateMsgBuilder.setEntity(JacksonUtil.toString(resource));
- resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
- testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder);
- uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build());
-
- testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, resource, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
- edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
+ edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
@@ -177,4 +162,35 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
tbResource.setEncodedData(TEST_DATA);
return tbResource;
}
+
+ private UplinkMsg getUplinkMsg(UUID uuid, TbResource tbResource, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
+ UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
+ ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder();
+ resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ resourceUpdateMsgBuilder.setEntity(JacksonUtil.toString(tbResource));
+ resourceUpdateMsgBuilder.setMsgType(updateMsgType);
+ testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder);
+ uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ return uplinkMsgBuilder.build();
+ }
+
+ private void checkResourceOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsg);
+
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
+ Assert.assertTrue(latestResponseMsg.getSuccess());
+
+ TbResource tb = doGet("/api/resource/" + uuid, TbResource.class);
+ Assert.assertNotNull(tb);
+ Assert.assertEquals(resourceTitle, tb.getName());
+ Assert.assertEquals(TEST_DATA, tb.getEncodedData());
+ }
+
}
diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewServiceTest.java
new file mode 100644
index 0000000000..aa6bfde935
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewServiceTest.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.entityview;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.objects.AttributesEntityView;
+import org.thingsboard.server.common.data.objects.TelemetryEntityView;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.entityview.EntityViewService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import java.util.List;
+import java.util.UUID;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+
+@ExtendWith(MockitoExtension.class)
+class DefaultTbEntityViewServiceTest {
+
+ final TenantId tenantId = TenantId.fromUUID(UUID.fromString("f09c8180-686c-11ef-9471-a71d33080e9c"));
+ final EntityId entityId = DeviceId.fromString("782aaab0-c7a8-11ef-a668-79582e785d5f");
+
+ @Mock
+ EntityViewService entityViewService;
+ @Mock
+ AttributesService attributesService;
+ @Mock
+ TelemetrySubscriptionService tsSubService;
+ @Mock
+ TimeseriesService tsService;
+
+ DefaultTbEntityViewService defaultTbEntityViewService;
+
+ @BeforeEach
+ void setup() {
+ defaultTbEntityViewService = new DefaultTbEntityViewService(entityViewService, attributesService, tsSubService, tsService);
+ }
+
+ @Test
+ void shouldNotSaveTimeseriesWhenCopyingLatestToEntityView() throws Exception {
+ // GIVEN
+ var entityView = new EntityView(new EntityViewId(UUID.randomUUID()));
+ entityView.setTenantId(tenantId);
+ entityView.setEntityId(entityId);
+ entityView.setKeys(new TelemetryEntityView(List.of("temperature"), new AttributesEntityView()));
+
+ List latest = List.of(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 22.3)));
+
+ given(tsService.findAll(eq(tenantId), eq(entityId), anyList())).willReturn(immediateFuture(latest));
+
+ // WHEN
+ defaultTbEntityViewService.updateEntityViewAttributes(tenantId, entityView, null, null);
+
+ // THEN
+ var captor = ArgumentCaptor.forClass(TimeseriesSaveRequest.class);
+ then(tsSubService).should().saveTimeseries(captor.capture());
+
+ var expectedCopyLatestRequest = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .entityId(entityView.getId())
+ .entries(latest)
+ .ttl(0L)
+ .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
+ .build();
+
+ var actualCopyLatestRequest = captor.getValue();
+
+ assertThat(actualCopyLatestRequest)
+ .usingRecursiveComparison()
+ .ignoringFields("callback")
+ .isEqualTo(expectedCopyLatestRequest);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
new file mode 100644
index 0000000000..10fdd85504
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java
@@ -0,0 +1,373 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.ApiUsageRecordKey;
+import org.thingsboard.server.common.data.ApiUsageState;
+import org.thingsboard.server.common.data.ApiUsageStateValue;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.objects.AttributesEntityView;
+import org.thingsboard.server.common.data.objects.TelemetryEntityView;
+import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.common.msg.queue.TbCallback;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.common.stats.TbApiUsageReportClient;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+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.service.apiusage.TbApiUsageStateService;
+import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
+import org.thingsboard.server.service.subscription.SubscriptionManagerService;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.lenient;
+
+@ExtendWith(MockitoExtension.class)
+class DefaultTelemetrySubscriptionServiceTest {
+
+ final TenantId tenantId = TenantId.fromUUID(UUID.fromString("a00ec470-c6b4-11ef-8c88-63b5533fb5bc"));
+ final CustomerId customerId = new CustomerId(UUID.fromString("7bdc9750-c775-11ef-8e03-ff69ed8da327"));
+ final EntityId entityId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088");
+
+ final long sampleTtl = 10_000L;
+
+ final List sampleTelemetry = List.of(
+ new BasicTsKvEntry(100L, new DoubleDataEntry("temperature", 65.2)),
+ new BasicTsKvEntry(100L, new DoubleDataEntry("humidity", 33.1))
+ );
+
+ ApiUsageState apiUsageState;
+
+ final TopicPartitionInfo tpi = TopicPartitionInfo.builder()
+ .tenantId(tenantId)
+ .myPartition(true)
+ .build();
+
+ final FutureCallback emptyCallback = new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {}
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {}
+ };
+
+ ExecutorService wsCallBackExecutor;
+ ExecutorService tsCallBackExecutor;
+
+ @Mock
+ TbClusterService clusterService;
+ @Mock
+ PartitionService partitionService;
+ @Mock
+ SubscriptionManagerService subscriptionManagerService;
+ @Mock
+ AttributesService attrService;
+ @Mock
+ TimeseriesService tsService;
+ @Mock
+ TbEntityViewService tbEntityViewService;
+ @Mock
+ TbApiUsageReportClient apiUsageClient;
+ @Mock
+ TbApiUsageStateService apiUsageStateService;
+
+ DefaultTelemetrySubscriptionService telemetryService;
+
+ @BeforeEach
+ void setup() {
+ telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService);
+ ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService);
+ ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService);
+ ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService));
+
+ wsCallBackExecutor = MoreExecutors.newDirectExecutorService();
+ ReflectionTestUtils.setField(telemetryService, "wsCallBackExecutor", wsCallBackExecutor);
+
+ tsCallBackExecutor = MoreExecutors.newDirectExecutorService();
+ ReflectionTestUtils.setField(telemetryService, "tsCallBackExecutor", tsCallBackExecutor);
+
+ apiUsageState = new ApiUsageState();
+ apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
+ lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState);
+
+ lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi);
+
+ lenient().when(tsService.save(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(sampleTelemetry.size()));
+ lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(sampleTelemetry.size()));
+ lenient().when(tsService.saveLatest(tenantId, entityId, sampleTelemetry)).thenReturn(immediateFuture(listOfNNumbers(sampleTelemetry.size())));
+
+ // mock no entity views
+ lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(Collections.emptyList()));
+
+ // send partition change event so currentPartitions set is populated
+ telemetryService.onTbApplicationEvent(new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of(new QueueKey(ServiceType.TB_CORE), Set.of(tpi))));
+ }
+
+ @AfterEach
+ void cleanup() {
+ wsCallBackExecutor.shutdownNow();
+ tsCallBackExecutor.shutdownNow();
+ }
+
+ @Test
+ void shouldReportStorageDataPointsApiUsageWhenTimeSeriesIsSaved() {
+ // GIVEN
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(new TimeseriesSaveRequest.Strategy(true, false, false))
+ .callback(emptyCallback)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ then(apiUsageClient).should().report(tenantId, customerId, ApiUsageRecordKey.STORAGE_DP_COUNT, sampleTelemetry.size());
+ }
+
+ @Test
+ void shouldNotReportStorageDataPointsApiUsageWhenTimeSeriesIsNotSaved() {
+ // GIVEN
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
+ .callback(emptyCallback)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ then(apiUsageClient).shouldHaveNoInteractions();
+ }
+
+ @Test
+ void shouldThrowStorageDisabledWhenTimeSeriesIsSavedAndStorageIsDisabled() {
+ // GIVEN
+ apiUsageState.setDbStorageState(ApiUsageStateValue.DISABLED);
+
+ SettableFuture future = SettableFuture.create();
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(TimeseriesSaveRequest.Strategy.SAVE_ALL)
+ .future(future)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ assertThat(future).failsWithin(Duration.ofSeconds(5))
+ .withThrowableOfType(ExecutionException.class)
+ .withCauseInstanceOf(RuntimeException.class)
+ .withMessageContaining("DB storage writes are disabled due to API limits!");
+ }
+
+ @Test
+ void shouldNotThrowStorageDisabledWhenTimeSeriesIsNotSavedAndStorageIsDisabled() {
+ // GIVEN
+ apiUsageState.setDbStorageState(ApiUsageStateValue.DISABLED);
+
+ SettableFuture future = SettableFuture.create();
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS)
+ .future(future)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ assertThat(future).succeedsWithin(Duration.ofSeconds(5));
+ }
+
+ @Test
+ void shouldCopyLatestToEntityViewWhenLatestIsSavedOnMainEntity() {
+ // GIVEN
+ var entityView = new EntityView(new EntityViewId(UUID.randomUUID()));
+ entityView.setTenantId(tenantId);
+ entityView.setCustomerId(customerId);
+ entityView.setEntityId(entityId);
+ entityView.setKeys(new TelemetryEntityView(sampleTelemetry.stream().map(KvEntry::getKey).toList(), new AttributesEntityView()));
+
+ // mock that there is one entity view
+ given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView)));
+ // mock that save latest call for entity view is successful
+ given(tsService.saveLatest(tenantId, entityView.getId(), sampleTelemetry)).willReturn(immediateFuture(listOfNNumbers(sampleTelemetry.size())));
+ // mock TPI for entity view
+ given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi);
+
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(new TimeseriesSaveRequest.Strategy(false, true, false))
+ .callback(emptyCallback)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ // should save latest to both the main entity and it's entity view
+ then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry);
+ then(tsService).should().saveLatest(tenantId, entityView.getId(), sampleTelemetry);
+ then(tsService).shouldHaveNoMoreInteractions();
+
+ // should send WS update only for entity view (WS update for the main entity is disabled in the save request)
+ then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityView.getId(), sampleTelemetry, TbCallback.EMPTY);
+ then(subscriptionManagerService).shouldHaveNoMoreInteractions();
+ }
+
+ @Test
+ void shouldNotCopyLatestToEntityViewWhenLatestIsNotSavedOnMainEntity() {
+ // GIVEN
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(new TimeseriesSaveRequest.Strategy(true, false, false))
+ .callback(emptyCallback)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ // should save only time series for the main entity
+ then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl);
+ then(tsService).shouldHaveNoMoreInteractions();
+
+ // should not send any WS updates
+ then(subscriptionManagerService).shouldHaveNoInteractions();
+ }
+
+ @ParameterizedTest
+ @MethodSource("booleanCombinations")
+ void shouldCallCorrectApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate) {
+ // GIVEN
+ var request = TimeseriesSaveRequest.builder()
+ .tenantId(tenantId)
+ .customerId(customerId)
+ .entityId(entityId)
+ .entries(sampleTelemetry)
+ .ttl(sampleTtl)
+ .strategy(new TimeseriesSaveRequest.Strategy(saveTimeseries, saveLatest, sendWsUpdate))
+ .callback(emptyCallback)
+ .build();
+
+ // WHEN
+ telemetryService.saveTimeseries(request);
+
+ // THEN
+ if (saveTimeseries && saveLatest) {
+ then(tsService).should().save(tenantId, entityId, sampleTelemetry, sampleTtl);
+ } else if (saveLatest) {
+ then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry);
+ } else if (saveTimeseries) {
+ then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl);
+ }
+ then(tsService).shouldHaveNoMoreInteractions();
+
+ if (sendWsUpdate) {
+ then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityId, sampleTelemetry, TbCallback.EMPTY);
+ } else {
+ then(subscriptionManagerService).shouldHaveNoInteractions();
+ }
+ }
+
+ private static Stream booleanCombinations() {
+ return Stream.of(
+ Arguments.of(true, true, true),
+ Arguments.of(true, true, false),
+ Arguments.of(true, false, true),
+ Arguments.of(true, false, false),
+ Arguments.of(false, true, true),
+ Arguments.of(false, true, false),
+ Arguments.of(false, false, true),
+ Arguments.of(false, false, false)
+ );
+ }
+
+ // used to emulate sequence numbers returned by save latest API
+ private static List listOfNNumbers(int N) {
+ return LongStream.range(0, N).boxed().toList();
+ }
+
+}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/DefaultTransportMBeanConfiguration.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/DefaultTransportMBeanConfiguration.java
new file mode 100644
index 0000000000..0bf8fdeede
--- /dev/null
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/DefaultTransportMBeanConfiguration.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jmx.export.MBeanExporter;
+import org.thingsboard.server.common.transport.service.DefaultTransportService;
+import org.thingsboard.server.queue.util.TbTransportComponent;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@TbTransportComponent
+@RequiredArgsConstructor
+public class DefaultTransportMBeanConfiguration {
+
+ private final DefaultTransportService transportService;
+
+ @Bean
+ public HashMapObserver hashMapObserver() {
+ return new HashMapObserver(transportService.sessions);
+ }
+
+ @Bean
+ public MBeanExporter mBeanExporter() {
+ MBeanExporter exporter = new MBeanExporter();
+ Map beans = new HashMap<>();
+ beans.put("org.thingsboard:type=TransportSessionMapObserver", hashMapObserver());
+ exporter.setBeans(beans);
+ return exporter;
+ }
+
+}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserver.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserver.java
new file mode 100644
index 0000000000..446ecb8aa4
--- /dev/null
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserver.java
@@ -0,0 +1,188 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.transport.service.SessionMetaData;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.transport.mqtt.session.GatewayDeviceSessionContext;
+
+import java.util.Map;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Slf4j
+public class HashMapObserver implements HashMapObserverMBean {
+ private final Map map;
+
+ @Override
+ public int getSize() {
+ return map.size();
+ }
+
+ @Override
+ public long getGatewayCount(String unused) {
+ return map.values().stream().filter(v-> v.getSessionInfo() != null && v.getSessionInfo().getIsGateway()).count();
+ }
+
+ @Override
+ public long getNonGatewayCount(String unused) {
+ return map.values().stream().filter(v-> v.getSessionInfo() != null && !v.getSessionInfo().getIsGateway()).count();
+ }
+
+ @Override
+ public String getSessionByUUID(String uuid) {
+ return String.valueOf(map.get(UUID.fromString(uuid)));
+ }
+
+ void addContent(Object entry, int count, StringBuilder content) {
+ String lineContent = String.valueOf(entry).replaceAll(System.lineSeparator()," ");
+ log.info("{} content = {}", count, lineContent);
+ content.append(lineContent).append(System.lineSeparator());
+ }
+
+ @Override
+ public String getAllSessions(String unused) {
+ log.info("getAllSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ addContent(entry, ++count, content);
+ }
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ @Override
+ public String getSubscribedSessions(String unused) {
+ log.info("getSubscribedSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ boolean hasSubscription = entry.getValue().isSubscribedToRPC() || entry.getValue().isSubscribedToAttributes();
+ if (hasSubscription) {
+ addContent(entry, ++count, content);
+ }
+ }
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ @Override
+ public String getNonActiveSessions(String unused) {
+ log.info("getNonActiveSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ SessionMetaData sessionMetaData = entry.getValue();
+ if (sessionMetaData.getListener() instanceof MqttTransportHandler) {
+ MqttTransportHandler listener = (MqttTransportHandler) sessionMetaData.getListener();
+ if (!listener.deviceSessionCtx.getChannel().channel().isActive()) {
+ addContent(entry, ++count, content);
+ }
+ }
+ }
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+ }
+
+ @Override
+ public String getActiveSessions(String unused) {
+ log.info("getActiveSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ SessionMetaData sessionMetaData = entry.getValue();
+ if (sessionMetaData.getListener() instanceof MqttTransportHandler) {
+ MqttTransportHandler listener = (MqttTransportHandler) sessionMetaData.getListener();
+ if (listener.deviceSessionCtx.getChannel().channel().isActive()) {
+ addContent(entry, ++count, content);
+ }
+ } else {
+ addContent(sessionMetaData.getListener().getClass(), ++count, content);
+ }
+ }
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+
+ throw e;
+ }
+ }
+
+ @Override
+ public String getGatewayDeviceSessionContextConnectedSessions(String unused) {
+ log.info("getGatewayDeviceSessionContextConnectedSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ SessionMetaData sessionMetaData = entry.getValue();
+ if (sessionMetaData.getListener() instanceof GatewayDeviceSessionContext) {
+ GatewayDeviceSessionContext listener = (GatewayDeviceSessionContext) sessionMetaData.getListener();
+ if (listener.isConnected()) {
+ addContent(entry, ++count, content);
+ }
+ }
+ }
+ addContent(count, count, content);
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+ }
+ @Override
+ public String getDeviceAwareSessionContextNotConnectedSessions(String unused) {
+ log.info("getDeviceAwareSessionContextNotConnectedSessions()");
+ StringBuilder content = new StringBuilder();
+ try {
+ int count = 0;
+ for (Map.Entry entry : map.entrySet()) {
+ SessionMetaData sessionMetaData = entry.getValue();
+ if (sessionMetaData.getListener() instanceof DeviceAwareSessionContext) {
+ DeviceAwareSessionContext listener = (DeviceAwareSessionContext) sessionMetaData.getListener();
+ if (!listener.isConnected()) {
+ addContent(entry, ++count, content);
+ }
+ }
+ }
+ addContent(count, count, content);
+ return content.toString();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ throw e;
+ }
+ }
+
+}
+
+// 4a7d85c9-eb4b-4fbc-8f6c-deb158cc9ac7=SessionMetaData(sessionInfo=nodeId: "bestia.local" sessionIdMSB: 5367593433178001340 sessionIdLSB: -8111863975520724281 tenantIdMSB: -1954197196874116625 tenantIdLSB: -7022192637061768255 deviceIdMSB: -5222516332438875665 deviceIdLSB: -7338416368958642691 deviceName: "Demo Device" deviceType: "default" gwSessionIdMSB: 3730140660294699909 gwSessionIdLSB: -7918622346767288875 deviceProfileIdMSB: -1952135612572036625 deviceProfileIdLSB: -7022192637061768255 customerIdMSB: 1405474927960789426 customerIdLSB: -9187201950435737472 gatewayIdMSB: 164837549830312431 gatewayIdLSB: -7338416368958642691 , sessionType=ASYNC, listener=GatewayDeviceSessionContext(super=AbstractGatewayDeviceSessionContext(super=MqttDeviceAwareSessionContext(super=DeviceAwareSessionContext(sessionId=4a7d85c9-eb4b-4fbc-8f6c-deb158cc9ac7, deviceId=b785e510-d34e-11ef-9a28-b5316a4ee5fd, tenantId=e4e14c00-d341-11ef-9e8c-29007391dbc1, deviceInfo=TransportDeviceInfo(tenantId=e4e14c00-d341-11ef-9e8c-29007391dbc1, customerId=13814000-1dd2-11b2-8080-808080808080, deviceProfileId=e4e89f00-d341-11ef-9e8c-29007391dbc1, deviceId=b785e510-d34e-11ef-9a28-b5316a4ee5fd, deviceName=Demo Device, deviceType=default, powerMode=null, additionalInfo={"lastConnectedGateway":"02499ee0-d348-11ef-9a28-b5316a4ee5fd"}, edrxCycle=null, psmActivityTimer=null, pagingTransmissionWindow=null, gateway=false), deviceProfile=DeviceProfile(tenantId=e4e14c00-d341-11ef-9e8c-29007391dbc1, name=default, description=Default device profile, isDefault=true, type=DEFAULT, transportType=DEFAULT, provisionType=DISABLED, defaultRuleChainId=null, defaultDashboardId=null, defaultQueueName=null, profileData=DeviceProfileData(configuration=DefaultDeviceProfileConfiguration(), transportConfiguration=DefaultDeviceProfileTransportConfiguration(), provisionConfiguration=DisabledDeviceProfileProvisionConfiguration(provisionDeviceSecret=null), alarms=null), provisionDeviceKey=null, firmwareId=null, softwareId=null, defaultEdgeRuleChainId=null, externalId=null, version=1), sessionInfo=nodeId: "bestia.local" sessionIdMSB: 5367593433178001340 sessionIdLSB: -8111863975520724281 tenantIdMSB: -1954197196874116625 tenantIdLSB: -7022192637061768255 deviceIdMSB: -5222516332438875665 deviceIdLSB: -7338416368958642691 deviceName: "Demo Device" deviceType: "default" gwSessionIdMSB: 3730140660294699909 gwSessionIdLSB: -7918622346767288875 deviceProfileIdMSB: -1952135612572036625 deviceProfileIdLSB: -7022192637061768255 customerIdMSB: 1405474927960789426 customerIdLSB: -9187201950435737472 gatewayIdMSB: 164837549830312431 gatewayIdLSB: -7338416368958642691 , connected=true), mqttQoSMap={org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@697a7658=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@d44c5be8=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@e3a8acd0=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@e3d2681a=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@a65b8616=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@133a8264=1, org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher@12e92606=1}), parent=org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler@3d4885c2, transportService=org.thingsboard.server.common.transport.service.DefaultTransportService@77c16f87)), scheduledFuture=null, subscribedToAttributes=true, subscribedToRPC=true, overwriteActivityTime=false)
+//1
\ No newline at end of file
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserverMBean.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserverMBean.java
new file mode 100644
index 0000000000..3a70674aec
--- /dev/null
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/HashMapObserverMBean.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+public interface HashMapObserverMBean {
+ int getSize();
+
+ long getGatewayCount(String unused);
+
+ long getNonGatewayCount(String unused);
+
+ String getSessionByUUID(String key);
+
+ String getAllSessions(String key);
+
+ String getSubscribedSessions(String unused);
+
+ String getNonActiveSessions(String unused);
+
+ String getActiveSessions(String unused);
+
+ String getGatewayDeviceSessionContextConnectedSessions(String unused);
+
+ String getDeviceAwareSessionContextNotConnectedSessions(String unused);
+}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index e669016514..e6efcf8206 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -963,7 +963,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
unSubResults.add((short) MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue());
}
}
- if (!activityReported) {
+ if (!activityReported && !deviceSessionCtx.isProvisionOnly()) {
transportService.recordActivity(deviceSessionCtx.getSessionInfo());
}
ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId(), unSubResults));
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
index e174ad86bd..1570bb4b7f 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
@@ -17,6 +17,8 @@ package org.thingsboard.server.transport.mqtt.session;
import io.netty.channel.ChannelFuture;
import io.netty.handler.codec.mqtt.MqttMessage;
+import lombok.Getter;
+import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
@@ -34,9 +36,11 @@ import java.util.concurrent.ConcurrentMap;
/**
* Created by ashvayka on 19.01.17.
*/
+@ToString(callSuper = true)
@Slf4j
public abstract class AbstractGatewayDeviceSessionContext extends MqttDeviceAwareSessionContext implements SessionMsgListener {
+ @Getter
protected final T parent;
private final TransportService transportService;
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
index 2e205ff840..8713e79988 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
@@ -16,8 +16,10 @@
package org.thingsboard.server.transport.mqtt.session;
import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -115,6 +117,7 @@ public abstract class AbstractGatewaySessionHandler devices;
private final ConcurrentMap> deviceFutures;
protected final ConcurrentMap mqttQoSMap;
+ @Getter
protected final ChannelHandlerContext channel;
protected final DeviceSessionCtx deviceSessionCtx;
protected final GatewayMetricsService gatewayMetricsService;
@@ -124,6 +127,7 @@ public abstract class AbstractGatewaySessionHandler {
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(T result) {
+ log.debug("[{}] Gateway disconnect [{}] device deregister callback [{}]", gateway.getTenantId(), gateway.getDeviceId(), name);
+ deregisterSession(name, result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+
+ }
+ }, MoreExecutors.directExecutor());
+ });
+
+ devices.forEach(this::deregisterSession);
+ } catch (Exception e) {
+ log.error("Gateway disconnect failure", e);
+ }
}
public void onDeviceDeleted(String deviceName) {
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java
index dff4245566..c5af48fadb 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.mqtt.session;
+import lombok.ToString;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
@@ -24,6 +25,7 @@ import java.util.concurrent.ConcurrentMap;
/**
* Created by nickAS21 on 26.12.22
*/
+@ToString(callSuper = true)
public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext {
public GatewayDeviceSessionContext(GatewaySessionHandler parent,
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
index 8ff3580753..ba8c8cb9fa 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.transport.mqtt.session;
import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.ToString;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
import java.util.List;
@@ -27,6 +28,7 @@ import java.util.stream.Collectors;
/**
* Created by ashvayka on 30.08.18.
*/
+@ToString(callSuper = true)
public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext {
private final ConcurrentMap mqttQoSMap;
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
index af9dca8583..547eab6172 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
@@ -735,7 +735,11 @@ public class DefaultTransportService extends TransportActivityManager implements
}
private void recordActivityInternal(TransportProtos.SessionInfoProto sessionInfo) {
- onActivity(toSessionId(sessionInfo), sessionInfo, getCurrentTimeMillis());
+ if (sessionInfo != null) {
+ onActivity(toSessionId(sessionInfo), sessionInfo, getCurrentTimeMillis());
+ } else {
+ log.warn("Session info is missing, unable to record activity");
+ }
}
@Override
diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
index 4841f05471..365fe23079 100644
--- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
+++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
@@ -450,7 +450,7 @@ public class GitRepository {
}
ObjectId result = git.getRepository().resolve(rev);
if (result == null) {
- throw new IllegalArgumentException("Failed to parse git revision string: \"" + rev + "\"");
+ throw new IllegalArgumentException("Failed to resolve '" + rev + "'");
}
return result;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
index 16088e83c9..1a4c3ffab2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
@@ -372,25 +372,6 @@ public class DefaultNotifications {
.build())
.build();
- public static final DefaultNotification queueTypeDeprecation = DefaultNotification.builder()
- .name("Queue type deprecation")
- .type(NotificationType.GENERAL)
- .subject("WARNING: ${queueType} deprecation")
- .text("Starting with ThingsBoard 4.0, ${queueType} will no longer be supported as a message queue for microservices. " +
- "Please migrate to Apache Kafka. This change will not impact any rule nodes.")
- .icon("warning").color(RED_COLOR)
- .build();
-
- public static final DefaultNotification databaseTypeDeprecation = DefaultNotification.builder()
- .name("Database type deprecation")
- .type(NotificationType.GENERAL)
- .subject("WARNING: ${databaseType} deprecation")
- .text("Starting with ThingsBoard 4.0, ${databaseType} will no longer be supported as a storage provider. " +
- "Please migrate to Cassandra or PostgreSQL.")
- .icon("warning")
- .color(RED_COLOR)
- .build();
-
private final NotificationTemplateService templateService;
private final NotificationRuleService ruleService;
diff --git a/monitoring/src/main/resources/root_rule_chain.json b/monitoring/src/main/resources/root_rule_chain.json
index ff44ebfe79..46bdc72d9f 100644
--- a/monitoring/src/main/resources/root_rule_chain.json
+++ b/monitoring/src/main/resources/root_rule_chain.json
@@ -21,9 +21,13 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"singletonMode": false,
- "configurationVersion": 0,
+ "configurationVersion": 1,
"configuration": {
- "defaultTTL": 0
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
},
"externalId": null
},
@@ -273,9 +277,13 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"singletonMode": false,
- "configurationVersion": 0,
+ "configurationVersion": 1,
"configuration": {
- "defaultTTL": 0
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
},
"externalId": null
},
@@ -307,11 +315,13 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries with TTL",
"singletonMode": false,
- "configurationVersion": 0,
+ "configurationVersion": 1,
"configuration": {
"defaultTTL": 180,
- "skipLatestPersistence": null,
- "useServerTs": null
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
},
"externalId": null
}
@@ -415,4 +425,4 @@
],
"ruleChainConnections": null
}
-}
\ No newline at end of file
+}
diff --git a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json
index c2bb52514e..c09da6aebb 100644
--- a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json
+++ b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json
@@ -36,11 +36,13 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "save timeseries",
"singletonMode": false,
- "configurationVersion": 0,
+ "configurationVersion": 1,
"configuration": {
"defaultTTL": 0,
- "skipLatestPersistence": false,
- "useServerTs": false
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
},
"externalId": null
},
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
index b8fb9b61ff..74c98cdec5 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
@@ -36,7 +36,7 @@ public interface NotificationCenter {
NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest, FutureCallback callback);
- void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template, GeneralNotificationInfo info);
+ void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template, GeneralNotificationInfo info); // for future use
void sendSystemNotification(TenantId tenantId, NotificationTargetId targetId, NotificationType type, NotificationInfo info); // for future use and compatibility with PE
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java
index 2b5881212d..fb667fbfb2 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java
@@ -38,10 +38,18 @@ public class TimeseriesSaveRequest {
private final EntityId entityId;
private final List entries;
private final long ttl;
- private final boolean saveLatest;
- private final boolean onlyLatest;
+ private final Strategy strategy;
private final FutureCallback callback;
+ public record Strategy(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate) {
+
+ public static final Strategy SAVE_ALL = new Strategy(true, true, true);
+ public static final Strategy WS_ONLY = new Strategy(false, false, true);
+ public static final Strategy LATEST_AND_WS = new Strategy(false, true, true);
+ public static final Strategy SKIP_ALL = new Strategy(false, false, false);
+
+ }
+
public static Builder builder() {
return new Builder();
}
@@ -53,9 +61,8 @@ public class TimeseriesSaveRequest {
private EntityId entityId;
private List entries;
private long ttl;
+ private Strategy strategy = Strategy.SAVE_ALL;
private FutureCallback callback;
- private boolean saveLatest = true;
- private boolean onlyLatest;
Builder() {}
@@ -92,14 +99,8 @@ public class TimeseriesSaveRequest {
return this;
}
- public Builder saveLatest(boolean saveLatest) {
- this.saveLatest = saveLatest;
- return this;
- }
-
- public Builder onlyLatest(boolean onlyLatest) {
- this.onlyLatest = onlyLatest;
- this.saveLatest = true;
+ public Builder strategy(Strategy strategy) {
+ this.strategy = strategy;
return this;
}
@@ -123,7 +124,7 @@ public class TimeseriesSaveRequest {
}
public TimeseriesSaveRequest build() {
- return new TimeseriesSaveRequest(tenantId, customerId, entityId, entries, ttl, saveLatest, onlyLatest, callback);
+ return new TimeseriesSaveRequest(tenantId, customerId, entityId, entries, ttl, strategy, callback);
}
}
diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java
new file mode 100644
index 0000000000..321892991e
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.api;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TimeseriesSaveRequestTest {
+
+ @Test
+ void testDefaultSaveStrategyIsSaveAll() {
+ var request = TimeseriesSaveRequest.builder().build();
+
+ assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL);
+ }
+
+ @Test
+ void testSaveAllStrategy() {
+ assertThat(TimeseriesSaveRequest.Strategy.SAVE_ALL).isEqualTo(new TimeseriesSaveRequest.Strategy(true, true, true));
+ }
+
+ @Test
+ void testWsOnlyStrategy() {
+ assertThat(TimeseriesSaveRequest.Strategy.WS_ONLY).isEqualTo(new TimeseriesSaveRequest.Strategy(false, false, true));
+ }
+
+ @Test
+ void testLatestAndWsStrategy() {
+ assertThat(TimeseriesSaveRequest.Strategy.LATEST_AND_WS).isEqualTo(new TimeseriesSaveRequest.Strategy(false, true, true));
+ }
+
+ @Test
+ void testSkipAllStrategy() {
+ assertThat(TimeseriesSaveRequest.Strategy.SKIP_ALL).isEqualTo(new TimeseriesSaveRequest.Strategy(false, false, false));
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
index ed1ed4fa83..4745147de8 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
@@ -347,7 +347,7 @@ public class TbHttpClient {
if (CredentialsType.BASIC == credentials.getType()) {
BasicCredentials basicCredentials = (BasicCredentials) credentials;
String authString = basicCredentials.getUsername() + ":" + basicCredentials.getPassword();
- String encodedAuthString = new String(Base64.getDecoder().decode(authString.getBytes(StandardCharsets.UTF_8)));
+ String encodedAuthString = new String(Base64.getEncoder().encode(authString.getBytes(StandardCharsets.UTF_8)));
headers.add("Authorization", "Basic " + encodedAuthString);
}
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
index b0bdb1de69..1b3acb6dae 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
@@ -15,8 +15,11 @@
*/
package org.thingsboard.rule.engine.telemetry;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@@ -24,6 +27,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.rule.engine.telemetry.strategy.ProcessingStrategy;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
@@ -32,13 +36,20 @@ import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Deduplicate;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.OnEveryMessage;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.WebSocketsOnly;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@Slf4j
@@ -46,20 +57,52 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE
type = ComponentType.ACTION,
name = "save time series",
configClazz = TbMsgTimeseriesNodeConfiguration.class,
- nodeDescription = "Saves time series data",
- nodeDetails = "Saves time series telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " +
- "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " +
- "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " +
- " " +
- "Enable 'useServerTs' param to use the timestamp of the message processing instead of the timestamp from the message. " +
- "Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" +
- " " +
- "In the case of sequential processing, the platform guarantees that the messages are processed in the order of their submission to the queue. " +
- "However, the timestamp of the messages originated by multiple devices/servers may be unsynchronized long before they are pushed to the queue. " +
- "The DB layer has certain optimizations to ignore the updates of the \"attributes\" and \"latest values\" tables if the new record has a timestamp that is older than the previous record. " +
- "So, to make sure that all the messages will be processed correctly, one should enable this parameter for sequential message processing scenarios.",
+ nodeDescription = """
+ Saves time series data with a configurable TTL and according to configured processing strategies.
+ """,
+ nodeDetails = """
+ Node performs three actions:
+
+ Time series: save time series data to a ts_kv table in a DB.
+ Latest values: save time series data to a ts_kv_latest table in a DB.
+ WebSockets: notify WebSockets subscriptions about time series data updates.
+
+
+ For each action , three processing strategies are available:
+
+ On every message: perform the action for every message.
+ Deduplicate: perform the action only for the first message from a particular originator within a configurable interval.
+ Skip: never perform the action.
+
+
+ Processing strategies are configured using processing settings , which support two modes:
+
+ Basic
+
+ On every message: applies the "On every message" strategy to all actions.
+ Deduplicate: applies the "Deduplicate" strategy (with a specified interval) to all actions.
+ WebSockets only: applies the "Skip" strategy to Time series and Latest values, and the "On every message" strategy to WebSockets.
+
+
+ Advanced: configure each action’s strategy independently.
+
+
+ By default, the timestamp is taken from metadata.ts. You can enable
+ Use server timestamp to always use the current server time instead. This is particularly
+ useful in sequential processing scenarios where messages may arrive with out-of-order timestamps from
+ multiple sources. Note that the DB layer may ignore older records for attributes and latest values,
+ so enabling Use server timestamp can ensure correct ordering.
+
+ The TTL is taken first from metadata.TTL. If absent, the node configuration’s default
+ TTL is used. If neither is set, the tenant profile default applies.
+
+ This node expects messages of type POST_TELEMETRY_REQUEST.
+
+ Output connections: Success, Failure.
+ """,
configDirective = "tbActionNodeTimeseriesConfig",
- icon = "file_upload"
+ icon = "file_upload",
+ version = 1
)
public class TbMsgTimeseriesNode implements TbNode {
@@ -67,15 +110,18 @@ public class TbMsgTimeseriesNode implements TbNode {
private TbContext ctx;
private long tenantProfileDefaultStorageTtl;
+ private ProcessingSettings processingSettings;
+
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgTimeseriesNodeConfiguration.class);
this.ctx = ctx;
ctx.addTenantProfileListener(this::onTenantProfileUpdate);
onTenantProfileUpdate(ctx.getTenantProfile());
+ processingSettings = config.getProcessingSettings();
}
- void onTenantProfileUpdate(TenantProfile tenantProfile) {
+ private void onTenantProfileUpdate(TenantProfile tenantProfile) {
DefaultTenantProfileConfiguration configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(configuration.getDefaultStorageTtlDays());
}
@@ -87,6 +133,15 @@ public class TbMsgTimeseriesNode implements TbNode {
return;
}
long ts = computeTs(msg, config.isUseServerTs());
+
+ TimeseriesSaveRequest.Strategy strategy = determineSaveStrategy(ts, msg.getOriginator().getId());
+
+ // short-circuit
+ if (!strategy.saveTimeseries() && !strategy.saveLatest() && !strategy.sendWsUpdate()) {
+ ctx.tellSuccess(msg);
+ return;
+ }
+
String src = msg.getData();
Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(src), ts);
if (tsKvMap.isEmpty()) {
@@ -110,7 +165,7 @@ public class TbMsgTimeseriesNode implements TbNode {
.entityId(msg.getOriginator())
.entries(tsKvEntryList)
.ttl(ttl)
- .saveLatest(!config.isSkipLatestPersistence())
+ .strategy(strategy)
.callback(new TelemetryNodeCallback(ctx, msg))
.build());
}
@@ -119,9 +174,56 @@ public class TbMsgTimeseriesNode implements TbNode {
return ignoreMetadataTs ? System.currentTimeMillis() : msg.getMetaDataTs();
}
+ private TimeseriesSaveRequest.Strategy determineSaveStrategy(long ts, UUID originatorUuid) {
+ if (processingSettings instanceof OnEveryMessage) {
+ return TimeseriesSaveRequest.Strategy.SAVE_ALL;
+ }
+ if (processingSettings instanceof WebSocketsOnly) {
+ return TimeseriesSaveRequest.Strategy.WS_ONLY;
+ }
+ if (processingSettings instanceof Deduplicate deduplicate) {
+ boolean isFirstMsgInInterval = deduplicate.getProcessingStrategy().shouldProcess(ts, originatorUuid);
+ return isFirstMsgInInterval ? TimeseriesSaveRequest.Strategy.SAVE_ALL : TimeseriesSaveRequest.Strategy.SKIP_ALL;
+ }
+ if (processingSettings instanceof Advanced advanced) {
+ return new TimeseriesSaveRequest.Strategy(
+ advanced.timeseries().shouldProcess(ts, originatorUuid),
+ advanced.latest().shouldProcess(ts, originatorUuid),
+ advanced.webSockets().shouldProcess(ts, originatorUuid)
+ );
+ }
+ // should not happen
+ throw new IllegalArgumentException("Unknown processing settings type: " + processingSettings.getClass().getSimpleName());
+ }
+
@Override
public void destroy() {
ctx.removeListeners();
}
+ @Override
+ public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
+ boolean hasChanges = false;
+ switch (fromVersion) {
+ case 0:
+ hasChanges = true;
+ JsonNode skipLatestPersistence = oldConfiguration.get("skipLatestPersistence");
+ if (skipLatestPersistence != null && "true".equals(skipLatestPersistence.asText())) {
+ var skipLatestProcessingSettings = new Advanced(
+ ProcessingStrategy.onEveryMessage(),
+ ProcessingStrategy.skip(),
+ ProcessingStrategy.onEveryMessage()
+ );
+ ((ObjectNode) oldConfiguration).set("processingSettings", JacksonUtil.valueToTree(skipLatestProcessingSettings));
+ } else {
+ ((ObjectNode) oldConfiguration).set("processingSettings", JacksonUtil.valueToTree(new OnEveryMessage()));
+ }
+ ((ObjectNode) oldConfiguration).remove("skipLatestPersistence");
+ break;
+ default:
+ break;
+ }
+ return new TbPair<>(hasChanges, oldConfiguration);
+ }
+
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java
index 1c33778a6b..88524e3751 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java
@@ -15,22 +15,84 @@
*/
package org.thingsboard.rule.engine.telemetry;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
+import lombok.Getter;
import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.telemetry.strategy.ProcessingStrategy;
+
+import java.util.Objects;
+
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Deduplicate;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.OnEveryMessage;
+import static org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration.ProcessingSettings.WebSocketsOnly;
@Data
public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration {
private long defaultTTL;
- private boolean skipLatestPersistence;
private boolean useServerTs;
+ @NotNull
+ private TbMsgTimeseriesNodeConfiguration.ProcessingSettings processingSettings;
@Override
public TbMsgTimeseriesNodeConfiguration defaultConfiguration() {
TbMsgTimeseriesNodeConfiguration configuration = new TbMsgTimeseriesNodeConfiguration();
configuration.setDefaultTTL(0L);
- configuration.setSkipLatestPersistence(false);
configuration.setUseServerTs(false);
+ configuration.setProcessingSettings(new OnEveryMessage());
return configuration;
}
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+ )
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = OnEveryMessage.class, name = "ON_EVERY_MESSAGE"),
+ @JsonSubTypes.Type(value = WebSocketsOnly.class, name = "WEBSOCKETS_ONLY"),
+ @JsonSubTypes.Type(value = Deduplicate.class, name = "DEDUPLICATE"),
+ @JsonSubTypes.Type(value = Advanced.class, name = "ADVANCED")
+ })
+ sealed interface ProcessingSettings permits OnEveryMessage, Deduplicate, WebSocketsOnly, Advanced {
+
+ record OnEveryMessage() implements ProcessingSettings {}
+
+ record WebSocketsOnly() implements ProcessingSettings {}
+
+ @Getter
+ final class Deduplicate implements ProcessingSettings {
+
+ private final int deduplicationIntervalSecs;
+
+ @JsonIgnore
+ private final ProcessingStrategy processingStrategy;
+
+ @JsonCreator
+ Deduplicate(@JsonProperty("deduplicationIntervalSecs") int deduplicationIntervalSecs) {
+ this.deduplicationIntervalSecs = deduplicationIntervalSecs;
+ processingStrategy = ProcessingStrategy.deduplicate(deduplicationIntervalSecs);
+ }
+
+ }
+
+ record Advanced(ProcessingStrategy timeseries, ProcessingStrategy latest, ProcessingStrategy webSockets) implements ProcessingSettings {
+
+ public Advanced {
+ Objects.requireNonNull(timeseries);
+ Objects.requireNonNull(latest);
+ Objects.requireNonNull(webSockets);
+ }
+
+ }
+
+ }
+
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategy.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategy.java
new file mode 100644
index 0000000000..a9c6fc38fd
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategy.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Longs;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.UUID;
+
+final class DeduplicateProcessingStrategy implements ProcessingStrategy {
+
+ private static final int MIN_DEDUPLICATION_INTERVAL_SECS = 1;
+ private static final int MAX_DEDUPLICATION_INTERVAL_SECS = (int) Duration.ofDays(1L).toSeconds();
+
+ private static final long MIN_INTERVAL_EXPIRY_MILLIS = Duration.ofMinutes(10L).toMillis();
+ private static final int INTERVAL_EXPIRY_FACTOR = 10;
+ private static final long MAX_INTERVAL_EXPIRY_MILLIS = Duration.ofDays(2L).toMillis();
+
+ private static final int MAX_TOTAL_INTERVALS_DURATION_SECS = (int) Duration.ofDays(2L).toSeconds();
+ private static final int MAX_NUMBER_OF_INTERVALS = 100;
+
+ private final long deduplicationIntervalMillis;
+ private final LoadingCache> deduplicationCache;
+
+ @JsonCreator
+ public DeduplicateProcessingStrategy(@JsonProperty("deduplicationIntervalSecs") int deduplicationIntervalSecs) {
+ if (deduplicationIntervalSecs < MIN_DEDUPLICATION_INTERVAL_SECS || deduplicationIntervalSecs > MAX_DEDUPLICATION_INTERVAL_SECS) {
+ throw new IllegalArgumentException("Deduplication interval must be at least " + MIN_DEDUPLICATION_INTERVAL_SECS + " second(s) " +
+ "and at most " + MAX_DEDUPLICATION_INTERVAL_SECS + " second(s), was " + deduplicationIntervalSecs + " second(s)");
+ }
+ deduplicationIntervalMillis = Duration.ofSeconds(deduplicationIntervalSecs).toMillis();
+ deduplicationCache = Caffeine.newBuilder()
+ .softValues()
+ .expireAfterAccess(calculateExpireAfterAccess(deduplicationIntervalSecs))
+ .maximumSize(calculateMaxNumberOfDeduplicationIntervals(deduplicationIntervalSecs))
+ .build(__ -> Sets.newConcurrentHashSet());
+ }
+
+ /**
+ * Calculates the expire-after-access duration. By default, we keep each deduplication interval
+ * alive for 10 “iterations” (interval duration × 10). However, we never let this drop below
+ * 10 minutes to ensure adequate retention for small intervals, nor exceed 48 hours to prevent
+ * storing stale data in memory.
+ */
+ private static Duration calculateExpireAfterAccess(int deduplicationIntervalSecs) {
+ long desiredExpiryMillis = Duration.ofSeconds(deduplicationIntervalSecs).toMillis() * INTERVAL_EXPIRY_FACTOR;
+ return Duration.ofMillis(Longs.constrainToRange(desiredExpiryMillis, MIN_INTERVAL_EXPIRY_MILLIS, MAX_INTERVAL_EXPIRY_MILLIS));
+ }
+
+ /**
+ * Calculates the maximum number of deduplication intervals we will store in the cache.
+ * We limit retention to two days to avoid stale data and cap it at 100 intervals to manage memory usage.
+ */
+ private static long calculateMaxNumberOfDeduplicationIntervals(int deduplicationIntervalSecs) {
+ int numberOfDeduplicationIntervals = MAX_TOTAL_INTERVALS_DURATION_SECS / deduplicationIntervalSecs;
+ return Math.min(numberOfDeduplicationIntervals, MAX_NUMBER_OF_INTERVALS);
+ }
+
+ @JsonProperty("deduplicationIntervalSecs")
+ public long getDeduplicationIntervalSecs() {
+ return Duration.ofMillis(deduplicationIntervalMillis).toSeconds();
+ }
+
+ @Override
+ public boolean shouldProcess(long ts, UUID originatorUuid) {
+ long intervalNumber = ts / deduplicationIntervalMillis;
+ return deduplicationCache.get(intervalNumber).add(originatorUuid);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategy.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategy.java
new file mode 100644
index 0000000000..9e8a87364a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategy.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import java.util.UUID;
+
+final class OnEveryMessageProcessingStrategy implements ProcessingStrategy {
+
+ private static final OnEveryMessageProcessingStrategy INSTANCE = new OnEveryMessageProcessingStrategy();
+
+ private OnEveryMessageProcessingStrategy() {}
+
+ @JsonCreator
+ public static OnEveryMessageProcessingStrategy getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean shouldProcess(long ts, UUID originatorUuid) {
+ return true;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategy.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategy.java
new file mode 100644
index 0000000000..724727dc22
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategy.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import java.util.UUID;
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = OnEveryMessageProcessingStrategy.class, name = "ON_EVERY_MESSAGE"),
+ @JsonSubTypes.Type(value = DeduplicateProcessingStrategy.class, name = "DEDUPLICATE"),
+ @JsonSubTypes.Type(value = SkipProcessingStrategy.class, name = "SKIP")
+})
+public sealed interface ProcessingStrategy permits OnEveryMessageProcessingStrategy, DeduplicateProcessingStrategy, SkipProcessingStrategy {
+
+ static ProcessingStrategy onEveryMessage() {
+ return OnEveryMessageProcessingStrategy.getInstance();
+ }
+
+ static ProcessingStrategy deduplicate(int deduplicationIntervalSecs) {
+ return new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+ }
+
+ static ProcessingStrategy skip() {
+ return SkipProcessingStrategy.getInstance();
+ }
+
+ boolean shouldProcess(long ts, UUID originatorUuid);
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategy.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategy.java
new file mode 100644
index 0000000000..66b966653d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategy.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import java.util.UUID;
+
+final class SkipProcessingStrategy implements ProcessingStrategy {
+
+ private static final SkipProcessingStrategy INSTANCE = new SkipProcessingStrategy();
+
+ private SkipProcessingStrategy() {}
+
+ @JsonCreator
+ public static SkipProcessingStrategy getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean shouldProcess(long ts, UUID originatorUuid) {
+ return false;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
index 360fbce5b0..63432afc37 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
@@ -533,7 +533,7 @@ public class TbMathNodeTest {
verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture());
verify(telemetryService, times(1)).saveTimeseries(assertArg(request -> {
assertThat(request.getEntries()).size().isOne();
- assertThat(request.isSaveLatest()).isTrue();
+ assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL);
}));
TbMsg resultMsg = msgCaptor.getValue();
@@ -569,7 +569,7 @@ public class TbMathNodeTest {
verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture());
verify(telemetryService, times(1)).saveTimeseries(assertArg(request -> {
assertThat(request.getEntries()).size().isOne();
- assertThat(request.isSaveLatest()).isTrue();
+ assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL);
}));
TbMsg resultMsg = msgCaptor.getValue();
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java
index 2cba4b8fb3..892e217414 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java
@@ -25,20 +25,23 @@ import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import org.mockito.ThrowingConsumer;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
+import org.thingsboard.rule.engine.telemetry.strategy.ProcessingStrategy;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
@@ -46,6 +49,8 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.ConstraintValidator;
import java.util.ArrayList;
import java.util.List;
@@ -55,24 +60,30 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
-public class TbMsgTimeseriesNodeTest {
+public class TbMsgTimeseriesNodeTest extends AbstractRuleNodeUpgradeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("e5095e9a-04f4-44c9-b443-1cf1b97d3384"));
- private final TenantProfileId TENANT_PROFILE_ID = new TenantProfileId(UUID.fromString("ab78dd78-83d0-43fa-869f-d42ec9ed1744"));
+
+ private TenantProfile tenantProfile;
private TbMsgTimeseriesNode node;
private TbMsgTimeseriesNodeConfiguration config;
- private long tenantProfileDefaultStorageTtl;
@Mock
private TbContext ctxMock;
@@ -81,29 +92,68 @@ public class TbMsgTimeseriesNodeTest {
@BeforeEach
public void setUp() throws TbNodeException {
- node = new TbMsgTimeseriesNode();
+ tenantProfile = new TenantProfile(new TenantProfileId(UUID.fromString("ab78dd78-83d0-43fa-869f-d42ec9ed1744")));
+ var tenantProfileConfiguration = new DefaultTenantProfileConfiguration();
+ tenantProfileConfiguration.setDefaultStorageTtlDays(5);
+ var tenantProfileData = new TenantProfileData();
+ tenantProfileData.setConfiguration(tenantProfileConfiguration);
+ tenantProfile.setProfileData(tenantProfileData);
+ lenient().when(ctxMock.getTenantProfile()).thenReturn(tenantProfile);
+
+ lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ lenient().when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
+
+ node = spy(new TbMsgTimeseriesNode());
config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration();
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getDefaultTTL()).isEqualTo(0L);
- assertThat(config.isSkipLatestPersistence()).isFalse();
+ assertThat(config.getProcessingSettings()).isInstanceOf(TbMsgTimeseriesNodeConfiguration.ProcessingSettings.OnEveryMessage.class);
assertThat(config.isUseServerTs()).isFalse();
}
+ @Test
+ public void whenInit_thenShouldAddTenantProfileListener() throws Exception {
+ // GIVEN-WHEN
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ // THEN
+ then(ctxMock).should().addTenantProfileListener(any());
+ }
+
+ @Test
+ public void givenProcessingSettingsAreNull_whenValidatingConstraints_thenThrowsException() {
+ // GIVEN
+ config.setProcessingSettings(null);
+
+ // WHEN-THEN
+ assertThatThrownBy(() -> ConstraintValidator.validateFields(config))
+ .isInstanceOf(DataValidationException.class)
+ .hasMessage("Validation error: processingSettings must not be null");
+ }
+
@ParameterizedTest
@EnumSource(TbMsgType.class)
public void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) throws TbNodeException {
- init();
+ // GIVEN
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
TbMsg msg = TbMsg.newMsg()
.type(msgType)
.originator(DEVICE_ID)
.copyMetaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_JSON_ARRAY)
.build();
+
+ // WHEN
node.onMsg(ctxMock, msg);
+ // THEN
+ then(ctxMock).should().addTenantProfileListener(any());
+ then(ctxMock).should().getTenantProfile();
+
ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
@@ -117,9 +167,11 @@ public class TbMsgTimeseriesNodeTest {
}
@Test
- public void givenTtlFromConfigIsZeroAndUseServiceTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException {
+ public void givenTtlFromConfigIsZeroAndUseServerTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException {
+ // GIVEN
config.setUseServerTs(true);
- init();
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
String data = """
{
@@ -134,24 +186,29 @@ public class TbMsgTimeseriesNodeTest {
.data(data)
.build();
- when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
- when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
doAnswer(invocation -> {
TimeseriesSaveRequest request = invocation.getArgument(0);
request.getCallback().onSuccess(null);
return null;
}).when(telemetryServiceMock).saveTimeseries(any(TimeseriesSaveRequest.class));
+ // WHEN
node.onMsg(ctxMock, msg);
+ // THEN
+ then(ctxMock).should().getTenantId();
+ then(ctxMock).should().getTelemetryService();
+ then(ctxMock).should().addTenantProfileListener(any());
+ then(ctxMock).should().getTenantProfile();
+
List expectedList = getTsKvEntriesListWithTs(data, System.currentTimeMillis());
verify(telemetryServiceMock).saveTimeseries(assertArg(request -> {
assertThat(request.getTenantId()).isEqualTo(TENANT_ID);
assertThat(request.getCustomerId()).isNull();
assertThat(request.getEntityId()).isEqualTo(DEVICE_ID);
assertThat(request.getEntries()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("ts").containsExactlyElementsOf(expectedList);
- assertThat(request.getTtl()).isEqualTo(tenantProfileDefaultStorageTtl);
- assertThat(request.isSaveLatest()).isTrue();
+ assertThat(request.getTtl()).isEqualTo(extractTtlAsSeconds(tenantProfile));
+ assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL);
assertThat(request.getCallback()).isInstanceOf(TelemetryNodeCallback.class);
}));
verify(ctxMock).tellSuccess(msg);
@@ -159,11 +216,17 @@ public class TbMsgTimeseriesNodeTest {
}
@Test
- public void givenSkipLatestPersistenceIsTrueAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException {
- long ttlFromConfig = 5L;
- config.setDefaultTTL(ttlFromConfig);
- config.setSkipLatestPersistence(true);
- init();
+ public void givenSkipLatestProcessingSettingsAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException {
+ // GIVEN
+ config.setDefaultTTL(10L);
+
+ var timeseriesStrategy = ProcessingStrategy.onEveryMessage();
+ var latestStrategy = ProcessingStrategy.skip();
+ var webSockets = ProcessingStrategy.onEveryMessage();
+ var processingSettings = new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced(timeseriesStrategy, latestStrategy, webSockets);
+ config.setProcessingSettings(processingSettings);
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
String data = """
{
@@ -180,24 +243,29 @@ public class TbMsgTimeseriesNodeTest {
.data(data)
.build();
- when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
- when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
doAnswer(invocation -> {
TimeseriesSaveRequest request = invocation.getArgument(0);
request.getCallback().onSuccess(null);
return null;
}).when(telemetryServiceMock).saveTimeseries(any(TimeseriesSaveRequest.class));
+ // WHEN
node.onMsg(ctxMock, msg);
+ // THEN
+ then(ctxMock).should().getTenantId();
+ then(ctxMock).should().getTelemetryService();
+ then(ctxMock).should().addTenantProfileListener(any());
+ then(ctxMock).should().getTenantProfile();
+
List expectedList = getTsKvEntriesListWithTs(data, ts);
verify(telemetryServiceMock).saveTimeseries(assertArg(request -> {
assertThat(request.getTenantId()).isEqualTo(TENANT_ID);
assertThat(request.getCustomerId()).isNull();
assertThat(request.getEntityId()).isEqualTo(DEVICE_ID);
assertThat(request.getEntries()).containsExactlyElementsOf(expectedList);
- assertThat(request.getTtl()).isEqualTo(ttlFromConfig);
- assertThat(request.isSaveLatest()).isFalse();
+ assertThat(request.getTtl()).isEqualTo(config.getDefaultTTL());
+ assertThat(request.getStrategy()).isEqualTo(new TimeseriesSaveRequest.Strategy(true, false, true));
assertThat(request.getCallback()).isInstanceOf(TelemetryNodeCallback.class);
}));
verify(ctxMock).tellSuccess(msg);
@@ -207,11 +275,10 @@ public class TbMsgTimeseriesNodeTest {
@ParameterizedTest
@MethodSource
public void givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl(String ttlFromMd, long ttlFromConfig, long expectedTtl) throws TbNodeException {
+ // GIVEN
config.setDefaultTTL(ttlFromConfig);
- init();
- when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
- when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
String data = """
{
@@ -227,14 +294,17 @@ public class TbMsgTimeseriesNodeTest {
.copyMetaData(metadata)
.data(data)
.build();
+
+ // WHEN
node.onMsg(ctxMock, msg);
+ // THEN
verify(telemetryServiceMock).saveTimeseries(assertArg(request -> {
assertThat(request.getTenantId()).isEqualTo(TENANT_ID);
assertThat(request.getCustomerId()).isNull();
assertThat(request.getEntityId()).isEqualTo(DEVICE_ID);
assertThat(request.getTtl()).isEqualTo(expectedTtl);
- assertThat(request.isSaveLatest()).isTrue();
+ assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL);
assertThat(request.getCallback()).isInstanceOf(TelemetryNodeCallback.class);
}));
}
@@ -251,26 +321,6 @@ public class TbMsgTimeseriesNodeTest {
);
}
- private void init() throws TbNodeException {
- var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
- var tenantProfile = getTenantProfile();
- when(ctxMock.getTenantProfile()).thenReturn(tenantProfile);
- tenantProfile.getProfileConfiguration().ifPresent(profileConfiguration ->
- tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(profileConfiguration.getDefaultStorageTtlDays()));
- node.init(ctxMock, configuration);
- verify(ctxMock).addTenantProfileListener(any());
- }
-
- private TenantProfile getTenantProfile() {
- var tenantProfile = new TenantProfile(TENANT_PROFILE_ID);
- var tenantProfileData = new TenantProfileData();
- var tenantProfileConfiguration = new DefaultTenantProfileConfiguration();
- tenantProfileConfiguration.setDefaultStorageTtlDays(5);
- tenantProfileData.setConfiguration(tenantProfileConfiguration);
- tenantProfile.setProfileData(tenantProfileData);
- return tenantProfile;
- }
-
private static List getTsKvEntriesListWithTs(String data, long ts) {
Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(data), ts);
List expectedList = new ArrayList<>();
@@ -282,4 +332,309 @@ public class TbMsgTimeseriesNodeTest {
return expectedList;
}
+ @Test
+ public void givenOnEveryMessageProcessingSettingsAndSameMessageTwoTimes_whenOnMsg_thenPersistSameMessageTwoTimes() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.OnEveryMessage());
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ var msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", "123")))
+ .build();
+
+ // WHEN-THEN
+ var expectedSaveRequest = TimeseriesSaveRequest.builder()
+ .tenantId(TENANT_ID)
+ .customerId(msg.getCustomerId())
+ .entityId(msg.getOriginator())
+ .entry(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 22.3)))
+ .ttl(extractTtlAsSeconds(tenantProfile))
+ .strategy(TimeseriesSaveRequest.Strategy.SAVE_ALL)
+ .build();
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(1)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(2)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+ }
+
+ @Test
+ public void givenDeduplicateProcessingSettingsAndSameMessageTwoTimes_whenOnMsg_thenPersistThisMessageOnlyFirstTime() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Deduplicate(10));
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ var msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", "123")))
+ .build();
+
+ // WHEN-THEN
+ var expectedSaveRequest = TimeseriesSaveRequest.builder()
+ .tenantId(TENANT_ID)
+ .customerId(msg.getCustomerId())
+ .entityId(msg.getOriginator())
+ .entry(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 22.3)))
+ .ttl(extractTtlAsSeconds(tenantProfile))
+ .strategy(TimeseriesSaveRequest.Strategy.SAVE_ALL)
+ .build();
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should().saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+
+ clearInvocations(telemetryServiceMock, ctxMock);
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(never()).saveTimeseries(any());
+ }
+
+ @Test
+ public void givenWebSocketsOnlyProcessingSettingsAndSameMessageTwoTimes_whenOnMsg_thenSendsOnlyWsUpdateTwoTimes() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.WebSocketsOnly());
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ var msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", "123")))
+ .build();
+
+ // WHEN-THEN
+ var expectedSaveRequest = TimeseriesSaveRequest.builder()
+ .tenantId(TENANT_ID)
+ .customerId(msg.getCustomerId())
+ .entityId(msg.getOriginator())
+ .entry(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 22.3)))
+ .ttl(extractTtlAsSeconds(tenantProfile))
+ .strategy(TimeseriesSaveRequest.Strategy.WS_ONLY)
+ .build();
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(1)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(2)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+ }
+
+ @Test
+ public void givenAdvancedProcessingSettingsWithOnEveryMessageStrategiesForAllActionsAndSameMessageTwoTimes_whenOnMsg_thenPersistSameMessageTwoTimes() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced(
+ ProcessingStrategy.onEveryMessage(),
+ ProcessingStrategy.onEveryMessage(),
+ ProcessingStrategy.onEveryMessage()
+ ));
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ var msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", "123")))
+ .build();
+
+ // WHEN-THEN
+ var expectedSaveRequest = TimeseriesSaveRequest.builder()
+ .tenantId(TENANT_ID)
+ .customerId(msg.getCustomerId())
+ .entityId(msg.getOriginator())
+ .entry(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 22.3)))
+ .ttl(extractTtlAsSeconds(tenantProfile))
+ .strategy(TimeseriesSaveRequest.Strategy.SAVE_ALL)
+ .build();
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(1)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(times(2)).saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest).usingRecursiveComparison().ignoringFields("callback").isEqualTo(expectedSaveRequest)
+ ));
+ }
+
+ @Test
+ public void givenAdvancedProcessingSettingsWithDifferentDeduplicateStrategyForEachAction_whenOnMsg_thenEvaluatesStrategiesForEachActionsIndependently() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced(
+ ProcessingStrategy.deduplicate(1),
+ ProcessingStrategy.deduplicate(2),
+ ProcessingStrategy.deduplicate(3)
+ ));
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ long ts1 = 500L;
+ long ts2 = 1500L;
+ long ts3 = 2500L;
+
+ // WHEN-THEN
+ node.onMsg(ctxMock, TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", Long.toString(ts1))))
+ .build());
+ then(telemetryServiceMock).should().saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.SAVE_ALL)
+ ));
+
+ clearInvocations(telemetryServiceMock);
+
+ node.onMsg(ctxMock, TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", Long.toString(ts2))))
+ .build());
+ then(telemetryServiceMock).should().saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(new TimeseriesSaveRequest.Strategy(true, false, false))
+ ));
+
+ clearInvocations(telemetryServiceMock);
+
+ node.onMsg(ctxMock, TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", Long.toString(ts3))))
+ .build());
+ then(telemetryServiceMock).should().saveTimeseries(assertArg(
+ actualSaveRequest -> assertThat(actualSaveRequest.getStrategy()).isEqualTo(new TimeseriesSaveRequest.Strategy(true, true, false))
+ ));
+ }
+
+ @Test
+ public void givenAdvancedProcessingSettingsWithSkipStrategiesForAllActionsAndSameMessageTwoTimes_whenOnMsg_thenSkipsSameMessageTwoTimes() throws TbNodeException {
+ // GIVEN
+ config.setProcessingSettings(new TbMsgTimeseriesNodeConfiguration.ProcessingSettings.Advanced(
+ ProcessingStrategy.skip(),
+ ProcessingStrategy.skip(),
+ ProcessingStrategy.skip()
+ ));
+
+ node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
+
+ var msg = TbMsg.newMsg()
+ .type(TbMsgType.POST_TELEMETRY_REQUEST)
+ .originator(DEVICE_ID)
+ .data(JacksonUtil.newObjectNode().put("temperature", 22.3).toString())
+ .metaData(new TbMsgMetaData(Map.of("ts", "123")))
+ .build();
+
+ // WHEN-THEN
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(never()).saveTimeseries(any());
+ then(ctxMock).should(times(1)).tellSuccess(msg);
+
+ node.onMsg(ctxMock, msg);
+ then(telemetryServiceMock).should(never()).saveTimeseries(any());
+ then(ctxMock).should(times(2)).tellSuccess(msg);
+ }
+
+ private static long extractTtlAsSeconds(TenantProfile tenantProfile) {
+ return TimeUnit.DAYS.toSeconds(tenantProfile.getDefaultProfileConfiguration().getDefaultStorageTtlDays());
+ }
+
+ @Override
+ protected TbNode getTestNode() {
+ return node;
+ }
+
+ private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
+ return Stream.of(
+ Arguments.of(0, """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "skipLatestPersistence": false
+ }""",
+ true,
+ """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
+ }"""),
+ Arguments.of(0, """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false
+ }""",
+ true,
+ """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
+ }"""),
+ Arguments.of(0, """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "skipLatestPersistence": null
+ }""",
+ true,
+ """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ON_EVERY_MESSAGE"
+ }
+ }"""),
+ Arguments.of(0, """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "skipLatestPersistence": true
+ }""",
+ true,
+ """
+ {
+ "defaultTTL": 0,
+ "useServerTs": false,
+ "processingSettings": {
+ "type": "ADVANCED",
+ "timeseries": {
+ "type": "ON_EVERY_MESSAGE"
+ },
+ "latest": {
+ "type": "SKIP"
+ },
+ "webSockets": {
+ "type": "ON_EVERY_MESSAGE"
+ }
+ }
+ }""")
+ );
+ }
+
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategyTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategyTest.java
new file mode 100644
index 0000000000..2c81ecd0a1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/DeduplicateProcessingStrategyTest.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import com.github.benmanes.caffeine.cache.Policy;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class DeduplicateProcessingStrategyTest {
+
+ final int deduplicationIntervalSecs = 10;
+
+ DeduplicateProcessingStrategy strategy;
+
+ @BeforeEach
+ void setup() {
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+ }
+
+ @Test
+ void shouldThrowWhenDeduplicationIntervalIsLessThanOneSecond() {
+ assertThatThrownBy(() -> new DeduplicateProcessingStrategy(0))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Deduplication interval must be at least 1 second(s) and at most 86400 second(s), was 0 second(s)");
+ }
+
+ @Test
+ void shouldThrowWhenDeduplicationIntervalIsMoreThan24Hours() {
+ assertThatThrownBy(() -> new DeduplicateProcessingStrategy(86401))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Deduplication interval must be at least 1 second(s) and at most 86400 second(s), was 86401 second(s)");
+ }
+
+ @Test
+ void shouldUseAtLeastTenMinutesForExpireAfterAccess() {
+ // GIVEN
+ int deduplicationIntervalSecs = 1; // min deduplication interval duration
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().expireAfterAccess())
+ .isPresent()
+ .map(Policy.FixedExpiration::getExpiresAfter)
+ .hasValue(Duration.ofMinutes(10L));
+ }
+
+ @Test
+ void shouldCalculateExpireAfterAccessAsIntervalDurationMultipliedByTen() {
+ // GIVEN
+ int deduplicationIntervalSecs = (int) Duration.ofHours(1L).toSeconds(); // max deduplication interval duration
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().expireAfterAccess())
+ .isPresent()
+ .map(Policy.FixedExpiration::getExpiresAfter)
+ .hasValue(Duration.ofHours(10L));
+ }
+
+ @Test
+ void shouldUseAtMostTwoDaysForExpireAfterAccess() {
+ // GIVEN
+ int deduplicationIntervalSecs = (int) Duration.ofDays(1L).toSeconds(); // max deduplication interval duration
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().expireAfterAccess())
+ .isPresent()
+ .map(Policy.FixedExpiration::getExpiresAfter)
+ .hasValue(Duration.ofDays(2L));
+ }
+
+ @Test
+ void shouldNotAllowMoreThan100DeduplicationIntervals() {
+ // GIVEN
+ int deduplicationIntervalSecs = 1; // min deduplication interval duration
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().eviction())
+ .isPresent()
+ .map(Policy.Eviction::getMaximum)
+ .hasValue(100L);
+ }
+
+ @Test
+ void shouldCalculateMaxIntervalsAsTwoDaysDividedByIntervalDuration() {
+ // GIVEN
+ int deduplicationIntervalSecs = (int) Duration.ofHours(1L).toSeconds();
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().eviction())
+ .isPresent()
+ .map(Policy.Eviction::getMaximum)
+ .hasValue(48L);
+ }
+
+ @Test
+ void shouldKeepAtLeastTwoDeduplicationIntervals() {
+ // GIVEN
+ int deduplicationIntervalSecs = (int) Duration.ofDays(1L).toSeconds(); // max deduplication interval duration
+
+ // WHEN
+ strategy = new DeduplicateProcessingStrategy(deduplicationIntervalSecs);
+
+ // THEN
+ var deduplicationCache = (LoadingCache>) ReflectionTestUtils.getField(strategy, "deduplicationCache");
+
+ assertThat(deduplicationCache.policy().eviction())
+ .isPresent()
+ .map(Policy.Eviction::getMaximum)
+ .hasValue(2L);
+ }
+
+ @Test
+ void shouldReturnTrueForFirstCallInInterval() {
+ long ts = 1_000_000L;
+ UUID originator = UUID.randomUUID();
+
+ assertThat(strategy.shouldProcess(ts, originator)).isTrue();
+ }
+
+ @Test
+ void shouldReturnFalseForSubsequentCallsInInterval() {
+ long baseTs = 1_000_000L;
+ UUID originator = UUID.randomUUID();
+
+ // Initial call should return true
+ assertThat(strategy.shouldProcess(baseTs, originator)).isTrue();
+
+ // Subsequent call within the same interval should return false for the same originator
+ long withinSameIntervalTs = baseTs + 1000L;
+ assertThat(strategy.shouldProcess(withinSameIntervalTs, originator)).isFalse();
+ }
+
+ @Test
+ void shouldHandleMultipleOriginatorsIndependently() {
+ long baseTs = 1_000_000L;
+ UUID originator1 = UUID.randomUUID();
+ UUID originator2 = UUID.randomUUID();
+
+ // First call for different originators in the same interval should return true independently
+ assertThat(strategy.shouldProcess(baseTs, originator1)).isTrue();
+ assertThat(strategy.shouldProcess(baseTs, originator2)).isTrue();
+
+ // Subsequent calls for the same originators within the same interval should return false
+ assertThat(strategy.shouldProcess(baseTs + 500L, originator1)).isFalse();
+ assertThat(strategy.shouldProcess(baseTs + 500L, originator2)).isFalse();
+ }
+
+ @Test
+ void shouldHandleEdgeCaseTimestamps() {
+ long minTs = Long.MIN_VALUE;
+ long maxTs = Long.MAX_VALUE;
+ UUID originator = UUID.randomUUID();
+
+ assertThat(strategy.shouldProcess(minTs, originator)).isTrue();
+ assertThat(strategy.shouldProcess(minTs + 1L, originator)).isFalse();
+
+ assertThat(strategy.shouldProcess(maxTs, originator)).isTrue();
+ assertThat(strategy.shouldProcess(maxTs - 1L, originator)).isFalse();
+ }
+
+ @Test
+ void shouldResetDeduplicationAtIntervalBoundaries() {
+ UUID originator = UUID.randomUUID();
+
+ // check 1st interval
+ long firstIntervalStart = 0L;
+ long firstIntervalEnd = firstIntervalStart + Duration.ofSeconds(deduplicationIntervalSecs).toMillis() - 1L;
+ long firstIntervalMiddle = calculateMiddle(firstIntervalStart, firstIntervalEnd);
+
+ assertThat(strategy.shouldProcess(firstIntervalStart, originator)).isTrue();
+ assertThat(strategy.shouldProcess(firstIntervalStart + 1, originator)).isFalse();
+ assertThat(strategy.shouldProcess(firstIntervalMiddle, originator)).isFalse();
+ assertThat(strategy.shouldProcess(firstIntervalEnd - 1, originator)).isFalse();
+ assertThat(strategy.shouldProcess(firstIntervalEnd, originator)).isFalse();
+
+ // check 2nd interval
+ long secondIntervalStart = firstIntervalEnd + 1L;
+ long secondIntervalEnd = secondIntervalStart + Duration.ofSeconds(deduplicationIntervalSecs).toMillis() - 1L;
+ long secondIntervalMiddle = calculateMiddle(secondIntervalStart, secondIntervalEnd);
+
+ assertThat(strategy.shouldProcess(secondIntervalStart, originator)).isTrue();
+ assertThat(strategy.shouldProcess(secondIntervalStart + 1, originator)).isFalse();
+ assertThat(strategy.shouldProcess(secondIntervalMiddle, originator)).isFalse();
+ assertThat(strategy.shouldProcess(secondIntervalEnd - 1, originator)).isFalse();
+ assertThat(strategy.shouldProcess(secondIntervalEnd, originator)).isFalse();
+ }
+
+ @Test
+ void shouldHandleMultipleOriginatorsOverMultipleIntervals() {
+ UUID originator1 = UUID.randomUUID();
+ UUID originator2 = UUID.randomUUID();
+ long baseTs = 0L;
+
+ // First interval for both originators
+ assertThat(strategy.shouldProcess(baseTs, originator1)).isTrue();
+ assertThat(strategy.shouldProcess(baseTs, originator2)).isTrue();
+
+ // Move to the next interval
+ long nextIntervalTs = baseTs + Duration.ofSeconds(10).toMillis();
+
+ // Each originator should be allowed again in the new interval
+ assertThat(strategy.shouldProcess(nextIntervalTs, originator1)).isTrue();
+ assertThat(strategy.shouldProcess(nextIntervalTs, originator2)).isTrue();
+
+ // Subsequent calls in the same new interval should return false
+ assertThat(strategy.shouldProcess(nextIntervalTs + 500L, originator1)).isFalse();
+ assertThat(strategy.shouldProcess(nextIntervalTs + 500L, originator2)).isFalse();
+ }
+
+ private static long calculateMiddle(long start, long end) {
+ return start + (end - start) / 2;
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategyTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategyTest.java
new file mode 100644
index 0000000000..36858418c3
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/OnEveryMessageProcessingStrategyTest.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OnEveryMessageProcessingStrategyTest {
+
+ @ParameterizedTest
+ @MethodSource("edgeCaseProvider")
+ void shouldAlwaysReturnTrueForAnyInput(long timestamp, UUID originator) {
+ var onEveryMessage = OnEveryMessageProcessingStrategy.getInstance();
+ assertThat(onEveryMessage.shouldProcess(timestamp, originator)).isTrue();
+ }
+
+ private static Stream edgeCaseProvider() {
+ return Stream.of(
+ Arguments.of(Long.MIN_VALUE, new UUID(0L, 0L)),
+ Arguments.of(Long.MAX_VALUE, new UUID(Long.MAX_VALUE, Long.MAX_VALUE)),
+ Arguments.of(0L, new UUID(0L, 0L)),
+ Arguments.of(-1L, new UUID(-1L, -1L)),
+ Arguments.of(1L, new UUID(1L, 1L)),
+ Arguments.of(42L, UUID.randomUUID()),
+ Arguments.of(1000L, null)
+ );
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategyTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategyTest.java
new file mode 100644
index 0000000000..45fed469d2
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/ProcessingStrategyTest.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ProcessingStrategyTest {
+
+ @Test
+ void testOnEveryMessageReturnsCorrectInstance() {
+ ProcessingStrategy strategy = ProcessingStrategy.onEveryMessage();
+ assertThat(strategy)
+ .isNotNull()
+ .isInstanceOf(OnEveryMessageProcessingStrategy.class);
+ }
+
+ @Test
+ void testDeduplicateReturnsCorrectInstance() {
+ int validDeduplicationIntervalSecs = 5;
+ ProcessingStrategy strategy = ProcessingStrategy.deduplicate(validDeduplicationIntervalSecs);
+ assertThat(strategy)
+ .isNotNull()
+ .isInstanceOf(DeduplicateProcessingStrategy.class);
+
+ long actualDeduplicationIntervalMillis = (long) ReflectionTestUtils.getField(strategy, "deduplicationIntervalMillis");
+ assertThat(actualDeduplicationIntervalMillis).isEqualTo(Duration.ofSeconds(validDeduplicationIntervalSecs).toMillis());
+ }
+
+ @Test
+ void testSkipReturnsCorrectInstance() {
+ ProcessingStrategy strategy = ProcessingStrategy.skip();
+ assertThat(strategy)
+ .isNotNull()
+ .isInstanceOf(SkipProcessingStrategy.class);
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategyTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategyTest.java
new file mode 100644
index 0000000000..ffdf2fc87e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/strategy/SkipProcessingStrategyTest.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.rule.engine.telemetry.strategy;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SkipProcessingStrategyTest {
+
+ @ParameterizedTest
+ @MethodSource("edgeCaseProvider")
+ void shouldAlwaysReturnFalseForAnyInput(long timestamp, UUID originator) {
+ var skipStrategy = SkipProcessingStrategy.getInstance();
+ assertThat(skipStrategy.shouldProcess(timestamp, originator)).isFalse();
+ }
+
+ private static Stream edgeCaseProvider() {
+ return Stream.of(
+ Arguments.of(Long.MIN_VALUE, new UUID(0L, 0L)),
+ Arguments.of(Long.MAX_VALUE, new UUID(Long.MAX_VALUE, Long.MAX_VALUE)),
+ Arguments.of(0L, new UUID(0L, 0L)),
+ Arguments.of(-1L, new UUID(-1L, -1L)),
+ Arguments.of(1L, new UUID(1L, 1L)),
+ Arguments.of(42L, UUID.randomUUID()),
+ Arguments.of(1000L, null)
+ );
+ }
+
+}
diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts
index 46cd2af7ab..daeb6b8faf 100644
--- a/ui-ngx/src/app/core/api/entity-data-subscription.ts
+++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts
@@ -685,7 +685,7 @@ export class EntityDataSubscription {
if (this.tsFields.length > 0) {
if (this.history) {
cmd.historyCmd = {
- keys: this.tsFields.map(key => key.key),
+ keys: [... new Set(this.tsFields.map(key => key.key))],
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs,
interval: 0,
@@ -702,7 +702,7 @@ export class EntityDataSubscription {
}
} else {
cmd.tsCmd = {
- keys: this.tsFields.map(key => key.key),
+ keys: [... new Set(this.tsFields.map(key => key.key))],
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow,
interval: 0,
diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts
index 749fb39e72..0e857c4570 100644
--- a/ui-ngx/src/app/core/http/rule-chain.service.ts
+++ b/ui-ngx/src/app/core/http/rule-chain.service.ts
@@ -33,6 +33,7 @@ import {
LinkLabel,
RuleNodeComponentDescriptor,
RuleNodeConfiguration,
+ RuleNodeConfigurationComponent,
ScriptLanguage,
TestScriptInputParams,
TestScriptResult
@@ -181,7 +182,7 @@ export class RuleChainService {
}
public registerSystemRuleNodeConfigModule(module: any) {
- Object.assign(this.ruleNodeConfigComponents, this.resourcesService.extractComponentsFromModule(module, true));
+ Object.assign(this.ruleNodeConfigComponents, this.resourcesService.extractComponentsFromModule(module, RuleNodeConfigurationComponent, true));
}
private loadRuleNodeComponents(ruleChainType: RuleChainType, config?: RequestConfig): Observable> {
diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts
index ebf390866c..030d53de6f 100644
--- a/ui-ngx/src/app/core/http/widget.service.ts
+++ b/ui-ngx/src/app/core/http/widget.service.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Injectable } from '@angular/core';
+import { Injectable, Type } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, of, ReplaySubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
@@ -24,7 +24,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
BaseWidgetType,
DeprecatedFilter,
- fullWidgetTypeFqn, migrateWidgetTypeToDynamicForms,
+ fullWidgetTypeFqn,
+ IWidgetSettingsComponent,
+ migrateWidgetTypeToDynamicForms,
+ WidgetSettingsComponent,
WidgetType,
widgetType,
WidgetTypeDetails,
@@ -36,6 +39,11 @@ import { filter, map, mergeMap, tap } from 'rxjs/operators';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { ActivationEnd, Router } from '@angular/router';
+import {
+ BasicWidgetConfigComponent,
+ IBasicWidgetConfigComponent
+} from '@home/components/widget/config/widget-config.component.models';
+import { ResourcesService } from '@core/services/resources.service';
@Injectable({
providedIn: 'root'
@@ -50,9 +58,13 @@ export class WidgetService {
private loadWidgetsBundleCacheSubject: ReplaySubject;
+ private basicWidgetSettingsComponentsMap: { [key: string]: Type } = {};
+ private widgetSettingsComponentsMap: { [key: string]: Type } = {};
+
constructor(
private http: HttpClient,
- private router: Router
+ private router: Router,
+ private resourcesService: ResourcesService,
) {
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
() => {
@@ -287,6 +299,30 @@ export class WidgetService {
this.widgetsInfoInMemoryCache.set(widgetInfo.fullFqn, widgetInfo);
}
+ public registerBasicWidgetConfigComponents(module: any) {
+ Object.assign(this.basicWidgetSettingsComponentsMap, this.resourcesService.extractComponentsFromModule(module, BasicWidgetConfigComponent));
+ }
+
+ public getBasicWidgetSettingsComponentBySelector(selector: string): Type {
+ return this.basicWidgetSettingsComponentsMap[selector];
+ }
+
+ public putBasicWidgetSettingsComponentToMap(selector: string, compType: Type) {
+ this.basicWidgetSettingsComponentsMap[selector] = compType;
+ }
+
+ public registerWidgetSettingsComponents(module: any) {
+ Object.assign(this.widgetSettingsComponentsMap, this.resourcesService.extractComponentsFromModule(module, WidgetSettingsComponent));
+ }
+
+ public getWidgetSettingsComponentTypeBySelector(selector: string): Type {
+ return this.widgetSettingsComponentsMap[selector];
+ }
+
+ public putWidgetSettingsComponentToMap(selector: string, compType: Type) {
+ this.widgetSettingsComponentsMap[selector] = compType;
+ }
+
private widgetTypeUpdated(updatedWidgetType: BaseWidgetType): void {
this.deleteWidgetInfoFromCache(fullWidgetTypeFqn(updatedWidgetType));
}
diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts
index 72cea30a1a..e2ee21c738 100644
--- a/ui-ngx/src/app/core/services/resources.service.ts
+++ b/ui-ngx/src/app/core/services/resources.service.ts
@@ -265,12 +265,15 @@ export class ResourcesService {
);
}
- public extractComponentsFromModule(module: any, isCamelCaseSelector = false): ComponentsSelectorMap {
+ public extractComponentsFromModule(module: any, instanceFilter?: any, isCamelCaseSelector = false): ComponentsSelectorMap {
const modulesWithComponents = this.extractModulesWithComponents(module);
const componentMap: ComponentsSelectorMap = {};
const processComponents = (components: Array<ɵComponentDef>) => {
components.forEach(item => {
+ if (instanceFilter && !(item.type.prototype instanceof instanceFilter)) {
+ return;
+ }
let selector = extractSelectorFromComponent(item);
if (isCamelCaseSelector) {
selector = camelCase(selector);
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts
index 775a5c963e..dbcd38a46f 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts
@@ -190,7 +190,7 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V
private fetchListObjects = (searchText: string): Observable> => {
this.searchText = searchText;
const pageLink = new PageLink(PAGE_SIZE_LIMIT, 0, this.searchText, {
- property: 'id',
+ property: 'resourceKey',
direction: Direction.ASC
});
return this.deviceProfileService.getLwm2mObjectsPage(pageLink);
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts
index 5e7b53c02b..e71055b5da 100644
--- a/ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts
@@ -42,6 +42,12 @@ import { DeleteAttributesConfigComponent } from './delete-attributes-config.comp
import { MathFunctionConfigComponent } from './math-function-config.component';
import { DeviceStateConfigComponent } from './device-state-config.component';
import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-config.component';
+import {
+ AdvancedPersistenceSettingComponent
+} from '@home/components/rule-node/action/advanced-persistence-setting.component';
+import {
+ AdvancedPersistenceSettingRowComponent
+} from '@home/components/rule-node/action/advanced-persistence-setting-row.component';
@NgModule({
declarations: [
@@ -67,7 +73,9 @@ import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-
PushToEdgeConfigComponent,
PushToCloudConfigComponent,
MathFunctionConfigComponent,
- DeviceStateConfigComponent
+ DeviceStateConfigComponent,
+ AdvancedPersistenceSettingComponent,
+ AdvancedPersistenceSettingRowComponent,
],
imports: [
CommonModule,
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html
new file mode 100644
index 0000000000..2bb51ad504
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html
@@ -0,0 +1,40 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts
new file mode 100644
index 0000000000..bf9b48aca3
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts
@@ -0,0 +1,114 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, forwardRef, Input } from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator
+} from '@angular/forms';
+import {
+ AdvancedProcessingConfig,
+ defaultAdvancedProcessingConfig,
+ maxDeduplicateTimeSecs,
+ ProcessingType,
+ ProcessingTypeTranslationMap
+} from '@home/components/rule-node/action/timeseries-config.models';
+import { isDefinedAndNotNull } from '@core/utils';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({
+ selector: 'tb-advanced-persistence-setting-row',
+ templateUrl: './advanced-persistence-setting-row.component.html',
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
+ multi: true
+ },{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
+ multi: true
+ }]
+})
+export class AdvancedPersistenceSettingRowComponent implements ControlValueAccessor, Validator {
+
+ @Input()
+ title: string;
+
+ persistenceSettingRowForm = this.fb.group({
+ type: [defaultAdvancedProcessingConfig.type],
+ deduplicationIntervalSecs: [{value: 60, disabled: true}]
+ });
+
+ PersistenceType = ProcessingType;
+ persistenceStrategies = [ProcessingType.ON_EVERY_MESSAGE, ProcessingType.DEDUPLICATE, ProcessingType.SKIP];
+ PersistenceTypeTranslationMap = ProcessingTypeTranslationMap;
+
+ maxDeduplicateTime = maxDeduplicateTimeSecs;
+
+ private propagateChange: (value: any) => void = () => {};
+
+ constructor(private fb: FormBuilder) {
+ this.persistenceSettingRowForm.get('type').valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe(() => this.updatedValidation());
+
+ this.persistenceSettingRowForm.valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe((value) => this.propagateChange(value));
+ }
+
+ registerOnChange(fn: any) {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(_fn: any) {
+ }
+
+ setDisabledState(isDisabled: boolean) {
+ if (isDisabled) {
+ this.persistenceSettingRowForm.disable({emitEvent: false});
+ } else {
+ this.persistenceSettingRowForm.enable({emitEvent: false});
+ this.updatedValidation();
+ }
+ }
+
+ validate(): ValidationErrors | null {
+ return this.persistenceSettingRowForm.valid ? null : {
+ persistenceSettingRow: false
+ };
+ }
+
+ writeValue(value: AdvancedProcessingConfig) {
+ if (isDefinedAndNotNull(value)) {
+ this.persistenceSettingRowForm.patchValue(value, {emitEvent: false});
+ } else {
+ this.persistenceSettingRowForm.patchValue(defaultAdvancedProcessingConfig);
+ }
+ }
+
+ private updatedValidation() {
+ if (this.persistenceSettingRowForm.get('type').value === ProcessingType.DEDUPLICATE) {
+ this.persistenceSettingRowForm.get('deduplicationIntervalSecs').enable({emitEvent: false});
+ } else {
+ this.persistenceSettingRowForm.get('deduplicationIntervalSecs').disable({emitEvent: false})
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html
new file mode 100644
index 0000000000..d3cbb1cdcb
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html
@@ -0,0 +1,36 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts
new file mode 100644
index 0000000000..969f432f5f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts
@@ -0,0 +1,82 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator
+} from '@angular/forms';
+import { Component, forwardRef } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { AdvancedProcessingStrategy } from '@home/components/rule-node/action/timeseries-config.models';
+
+@Component({
+ selector: 'tb-advanced-persistence-settings',
+ templateUrl: './advanced-persistence-setting.component.html',
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
+ multi: true
+ },{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
+ multi: true
+ }]
+})
+export class AdvancedPersistenceSettingComponent implements ControlValueAccessor, Validator {
+
+ persistenceForm = this.fb.group({
+ timeseries: [null],
+ latest: [null],
+ webSockets: [null]
+ });
+
+ private propagateChange: (value: any) => void = () => {};
+
+ constructor(private fb: FormBuilder) {
+ this.persistenceForm.valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe(value => this.propagateChange(value));
+ }
+
+ registerOnChange(fn: any) {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(_fn: any) {
+ }
+
+ setDisabledState(isDisabled: boolean) {
+ if (isDisabled) {
+ this.persistenceForm.disable({emitEvent: false});
+ } else {
+ this.persistenceForm.enable({emitEvent: false});
+ }
+ }
+
+ validate(): ValidationErrors | null {
+ return this.persistenceForm.valid ? null : {
+ persistenceForm: false
+ };
+ }
+
+ writeValue(value: AdvancedProcessingStrategy) {
+ this.persistenceForm.patchValue(value, {emitEvent: false});
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
index 31e827d331..715776d780 100644
--- a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
+++ b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
@@ -16,35 +16,74 @@
-->