diff --git a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json index 0cfcebef5d..272de5848f 100644 --- a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json +++ b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json @@ -564,6 +564,7 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, + "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { @@ -977,6 +978,7 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, + "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { 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 index f6d881b172..8e1a46978e 100644 --- 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 @@ -1,6 +1,6 @@ { "title": "HP 3 phase voltage relay", - "description": "Three phase voltage relay with various states and inications.", + "description": "Three phase voltage relay with various states and indications.", "searchTags": [ "energy", "power", @@ -39,7 +39,7 @@ }, { "tag": "firstPhaseValue", - "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.firstPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.firstPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -49,7 +49,7 @@ }, { "tag": "secondPhaseValue", - "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.secondPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.secondPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -59,12 +59,12 @@ }, { "tag": "thirdPhaseValue", - "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.thirdPhaseVoltage, 0, null, 0));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.thirdPhaseVoltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { @@ -488,6 +488,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "V", + "supportsUnitConversion": true, "disabled": false, "visible": true }, diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg index 794e6e61d9..f3c20f2324 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -464,8 +464,7 @@ "type": "units", "default": "m³/hr", "fieldClass": "medium-width", - "disabled": false, - "visible": true + "supportsUnitConversion": true }, { "id": "valueDecimals", diff --git a/application/src/main/data/json/system/scada_symbols/conical-tank.svg b/application/src/main/data/json/system/scada_symbols/conical-tank.svg index 6b37adc52e..592508ee30 100644 --- a/application/src/main/data/json/system/scada_symbols/conical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/conical-tank.svg @@ -159,96 +159,51 @@ "name": "Stand", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "tankColor", "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -261,64 +216,35 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] } diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index acb895cd5a..5675818fe7 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -312,8 +312,8 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -368,6 +368,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -390,16 +398,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg index b3506ae54f..7b11968223 100644 --- a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg @@ -43,7 +43,7 @@ }, { "tag": "maxValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -53,7 +53,7 @@ }, { "tag": "minValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -73,12 +73,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -329,16 +329,8 @@ "name": "{i18n:scada.symbol.min-max-value}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minValue", @@ -347,14 +339,9 @@ "default": 0, "required": true, "subLabel": "{i18n:scada.symbol.min-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxValue", @@ -363,30 +350,17 @@ "default": 100, "required": true, "subLabel": "{i18n:scada.symbol.max-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "value", "name": "{i18n:scada.symbol.value}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueDecimals", @@ -395,14 +369,12 @@ "default": 0, "required": true, "subLabel": "Decimals", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", "min": 0, "max": 10, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "valueFont", @@ -415,64 +387,36 @@ "weight": "400", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueColor", "name": "{i18n:scada.symbol.value}", "type": "color", "default": "#002878", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "label", "name": "{i18n:scada.symbol.label}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelText", "name": "{i18n:scada.symbol.label}", "type": "text", "default": "Outdoor", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", "fieldClass": "flex", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelFont", @@ -485,64 +429,35 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelColor", "name": "{i18n:scada.symbol.label}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showUnits", "name": "{i18n:scada.symbol.units}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "unitsFont", @@ -555,224 +470,119 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "unitsColor", "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "arrowColor", "name": "{i18n:scada.symbol.arrow-color}", "type": "color", "default": "#666666", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showTarget", "name": "{i18n:scada.symbol.target}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "targetColor", "name": "{i18n:scada.symbol.target}", "type": "color", "default": "#DEDEDE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showTarget", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "showHighCriticalScale", "name": "{i18n:scada.symbol.show-high-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showHighWarningScale", "name": "{i18n:scada.symbol.show-high-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showLowWarningScale", "name": "{i18n:scada.symbol.show-low-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showLowCriticalScale", "name": "{i18n:scada.symbol.show-low-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scaleColor", "name": "{i18n:scada.symbol.scale-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#EBEBEB", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#666666", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg index 3c52dae1c3..37ba3df7c2 100644 --- a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg @@ -43,7 +43,7 @@ }, { "tag": "maxValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -53,7 +53,7 @@ }, { "tag": "minValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -73,12 +73,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -329,16 +329,8 @@ "name": "{i18n:scada.symbol.min-max-value}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minValue", @@ -347,14 +339,9 @@ "default": 0, "required": true, "subLabel": "{i18n:scada.symbol.min-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxValue", @@ -363,30 +350,17 @@ "default": 100, "required": true, "subLabel": "{i18n:scada.symbol.max-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "value", "name": "{i18n:scada.symbol.value}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueDecimals", @@ -395,14 +369,11 @@ "default": 0, "required": true, "subLabel": "Decimals", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 10, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "valueFont", @@ -415,64 +386,33 @@ "weight": "400", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueColor", "name": "{i18n:scada.symbol.value}", "type": "color", "default": "#002878", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "label", "name": "{i18n:scada.symbol.label}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelText", "name": "{i18n:scada.symbol.label}", "type": "text", "default": "Outdoor", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", "fieldClass": "flex", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelFont", @@ -485,64 +425,34 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelColor", "name": "{i18n:scada.symbol.label}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showUnits", "name": "{i18n:scada.symbol.units}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "required": false, + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "unitsFont", @@ -555,224 +465,117 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "unitsColor", "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "arrowColor", "name": "{i18n:scada.symbol.arrow-color}", "type": "color", "default": "#666666", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showTarget", "name": "{i18n:scada.symbol.target}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "targetColor", "name": "{i18n:scada.symbol.target}", "type": "color", "default": "#DEDEDE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showTarget", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "showHighCriticalScale", "name": "{i18n:scada.symbol.show-high-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showHighWarningScale", "name": "{i18n:scada.symbol.show-high-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showLowWarningScale", "name": "{i18n:scada.symbol.show-low-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showLowCriticalScale", "name": "{i18n:scada.symbol.show-low-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scaleColor", "name": "{i18n:scada.symbol.scale-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#EBEBEB", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#666666", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 48668e9851..0d82a10b41 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 265;\n var majorIntervalLength = 895 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(825, y, 857, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 815, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(837, minorY, 857, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -308,7 +308,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -373,6 +373,15 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disableOnProperty": "scale", + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -386,16 +395,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", 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 index 606eb1153d..3855d66ec8 100644 --- 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 @@ -38,12 +38,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.measured, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.measured, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -353,6 +353,7 @@ "name": "{i18n:scada.symbol.label}", "type": "text", "default": "T1", + "disableOnProperty": "showLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -368,6 +369,7 @@ "weight": "400", "style": "normal" }, + "disableOnProperty": "showLabel", "disabled": false, "visible": true }, @@ -376,6 +378,7 @@ "name": "{i18n:scada.symbol.label}", "type": "color", "default": "#000", + "disableOnProperty": "showLabel", "disabled": false, "visible": true }, @@ -392,6 +395,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "kWh", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -406,6 +410,7 @@ "weight": "500", "style": "normal" }, + "disableOnProperty": "showUnits", "disabled": false, "visible": true }, @@ -414,6 +419,7 @@ "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000", + "disableOnProperty": "showUnits", "disabled": false, "visible": true }, 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 index bba67e5fe3..e095c1417f 100644 --- 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 @@ -38,7 +38,7 @@ }, { "tag": "export-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.exportValueFont, ctx.properties.exportValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.exportRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.exportValueFont, ctx.properties.exportValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.exportRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -48,7 +48,7 @@ }, { "tag": "night-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -58,7 +58,7 @@ }, { "tag": "off-peak-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -68,12 +68,12 @@ }, { "tag": "peak-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { @@ -533,6 +533,7 @@ "group": "{i18n:scada.symbol.off-peak-rate}", "type": "text", "default": "T1", + "disableOnProperty": "showOffPeakLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -549,6 +550,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showOffPeakLabel", "disabled": false, "visible": true }, @@ -558,6 +560,7 @@ "group": "{i18n:scada.symbol.off-peak-rate}", "type": "color", "default": "#000000", + "disableOnProperty": "showOffPeakLabel", "disabled": false, "visible": true }, @@ -609,6 +612,7 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "text", "default": "T2", + "disableOnProperty": "showNightLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -625,6 +629,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showNightLabel", "disabled": false, "visible": true }, @@ -634,6 +639,7 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "color", "default": "#000", + "disableOnProperty": "showNightLabel", "disabled": false, "visible": true }, @@ -685,6 +691,7 @@ "group": "{i18n:scada.symbol.peak-rate}", "type": "text", "default": "T3", + "disableOnProperty": "showPeakLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -701,6 +708,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showPeakLabel", "disabled": false, "visible": true }, @@ -710,6 +718,7 @@ "group": "{i18n:scada.symbol.peak-rate}", "type": "color", "default": "#000", + "disableOnProperty": "showPeakLabel", "disabled": false, "visible": true }, @@ -761,6 +770,7 @@ "group": "{i18n:scada.symbol.export-rate}", "type": "text", "default": "Export", + "disableOnProperty": "showExportLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -777,6 +787,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showExportLabel", "disabled": false, "visible": true }, @@ -786,6 +797,7 @@ "group": "{i18n:scada.symbol.export-rate}", "type": "color", "default": "#000", + "disableOnProperty": "showExportLabel", "disabled": false, "visible": true }, @@ -835,6 +847,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "kWh", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -849,6 +862,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showUnits", "disabled": false, "visible": true }, @@ -857,6 +871,7 @@ "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000", + "disableOnProperty": "showUnits", "disabled": false, "visible": true } diff --git a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg index eed54682cd..b1e643698c 100644 --- a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg @@ -62,7 +62,7 @@ }, { "tag": "value-text", - "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", + "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "actions": null }, { @@ -366,80 +366,49 @@ "name": "{i18n:scada.symbol.colors}", "type": "color", "default": "#FFFFFF", - "required": null, "subLabel": "{i18n:scada.symbol.running}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "stoppedColor", "name": "{i18n:scada.symbol.colors}", "type": "color", "default": "#666666", - "required": null, "subLabel": "{i18n:scada.symbol.stopped}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "{i18n:scada.symbol.warning}", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.alarm-colors}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "{i18n:scada.symbol.critical}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minTemperature", "name": "{i18n:scada.symbol.temperature}", "type": "number", "default": 10, - "required": null, "subLabel": "Min", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, "rowClass": "column-xs", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxTemperature", @@ -448,14 +417,9 @@ "default": 45, "required": true, "subLabel": "Max", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "temperatureStep", @@ -463,31 +427,17 @@ "type": "number", "default": 1, "required": true, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 0.5 + "step": 0.5, + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#002878", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -500,64 +450,33 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-units}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.value-units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueBoxBackground", "name": "{i18n:scada.symbol.value-box-background}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg index 5e0c781941..a2ca312730 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -464,6 +464,7 @@ "type": "units", "default": "m³/hr", "fieldClass": "medium-width", + "supportsUnitConversion": true, "disabled": false, "visible": true }, diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg index bb6f5e55a9..6b0df69f0d 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 592 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(208, y, 240, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 198, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 198, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 198, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(220, minorY, 240, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -346,6 +346,24 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": false, + "supportsUnitConversion": true, + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -359,16 +377,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index b6b6eede7b..c02da77258 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -312,7 +312,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -368,6 +368,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -390,16 +398,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg index c8b40477ef..6789f8f7c9 100644 --- a/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg @@ -160,96 +160,51 @@ "name": "Stand", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "tankColor", "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -262,64 +217,35 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "scale", "name": "{i18n:scada.symbol.scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "scale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index 69360fbe9c..c9d9361d7d 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -312,7 +312,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -368,6 +368,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -390,16 +398,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index 09c0e2a9e1..8e5c057209 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 60;\n var majorIntervalLength = 910 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(656, y, 688, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 646, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(668, minorY, 688, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -365,7 +365,17 @@ "value": false, "label": "Absolute" } - ] + ], + "disabled": false, + "visible": true + }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true }, { "id": "transparent", @@ -389,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index be8b1207a0..9b6763e0ea 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index cc168915ad..75ff5ef979 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 203;\n var majorIntervalLength = 763 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(676, y, 708, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 666, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(688, minorY, 708, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -312,7 +312,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -368,6 +368,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -390,16 +398,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg index 1c29a79479..e121adaffd 100644 --- a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,7 +463,8 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "fieldClass": "medium-width" + "fieldClass": "medium-width", + "supportsUnitConversion": true }, { "id": "valueDecimals", diff --git a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg index f1e982bac0..6232f3dda3 100644 --- a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg @@ -72,7 +72,7 @@ }, { "tag": "value-text", - "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", + "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "actions": null } ], @@ -432,13 +432,11 @@ "required": true, "subLabel": "Min", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, "rowClass": "column-xs", - "fieldClass": "", "min": 0, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxTemperature", @@ -447,14 +445,10 @@ "default": 45, "required": true, "subLabel": "Max", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "temperatureStep", @@ -462,31 +456,17 @@ "type": "number", "default": 1, "required": true, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 0.5 + "step": 0.5, + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#000000C2", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -499,192 +479,101 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-units}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.value-units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueBoxBackground", "name": "{i18n:scada.symbol.value-box-background}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "powerButtonBackground", "name": "{i18n:scada.symbol.power-button-background}", "type": "color", "default": "#1ABB48", - "required": null, "subLabel": "Enabled", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, "rowClass": "column-xs", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "disabledPowerButtonBackground", "name": "{i18n:scada.symbol.power-button-background}", "type": "color", "default": "#FFFFFF", - "required": null, "subLabel": "Disabled", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "runningColor", "name": "{i18n:scada.symbol.running-color}", "type": "color", "default": "#1C943E", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "stoppedColor", "name": "{i18n:scada.symbol.stopped-color}", "type": "color", "default": "#696969", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.warning-color}", "type": "color", "default": "#FAA405", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.critical-color}", "type": "color", "default": "#D12730", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "heatPumpColor", "name": "{i18n:scada.symbol.heat-pump-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/meter.svg b/application/src/main/data/json/system/scada_symbols/meter.svg index d5fa9f7cd7..6fdd27d2fa 100644 --- a/application/src/main/data/json/system/scada_symbols/meter.svg +++ b/application/src/main/data/json/system/scada_symbols/meter.svg @@ -51,7 +51,7 @@ }, { "tag": "scale", - "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 328 : 365;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 328 : 365;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -433,7 +433,7 @@ "type": "units", "default": "%", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -493,6 +493,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -510,7 +518,7 @@ "name": "{i18n:scada.symbol.major-ticks}", "type": "font", "default": { - "size": 12, + "size": 10, "sizeUnit": "px", "family": "Roboto", "weight": "500", diff --git a/application/src/main/data/json/system/scada_symbols/pool-hp.svg b/application/src/main/data/json/system/scada_symbols/pool-hp.svg index 0ce78af7d9..03865a040c 100644 --- a/application/src/main/data/json/system/scada_symbols/pool-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/pool-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 792 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(298, y, 330, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 288, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 288, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 288, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(310, minorY, 330, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -346,6 +346,24 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": false, + "supportsUnitConversion": true, + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -394,16 +412,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "minorColor", "name": "{i18n:scada.symbol.minor-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg index 6f8b12737c..75f21e9457 100644 --- a/application/src/main/data/json/system/scada_symbols/pool.svg +++ b/application/src/main/data/json/system/scada_symbols/pool.svg @@ -148,80 +148,43 @@ "name": "{i18n:scada.symbol.tank-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBox", "name": "{i18n:scada.symbol.value-box}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueBoxColor", "name": "{i18n:scada.symbol.value-box}", "type": "color", "default": "#F3F3F3", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-text}", "type": "units", "default": "gal", - "required": null, "subLabel": "{i18n:scada.symbol.units}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -234,48 +197,26 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#0000008A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "valueBox", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg index 6a201111ef..3984083640 100644 --- a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,7 +463,8 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "fieldClass": "medium-width" + "fieldClass": "medium-width", + "supportsUnitConversion": true }, { "id": "valueDecimals", diff --git a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg index b1aa081b82..a0802c6e66 100644 --- a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg @@ -72,7 +72,7 @@ }, { "tag": "value-text", - "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar units = ctx.properties.valueUnits ? ctx.properties.units : null;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, decimals ? 0 : 1, units, !decimals);\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", + "stateRenderFunction": "var valueTextFont = ctx.properties.valueTextFont;\nvar valueTextColor = ctx.properties.valueTextColor;\nvar currentVolume = ctx.values.temperature;\nvar decimals = Math.floor(ctx.properties.temperatureStep) === ctx.properties.temperatureStep;\nvar valueText = ctx.api.formatValue(currentVolume, {units: ctx.properties.units, decimals: decimals ? 0 : 1, ignoreUnitSymbol: !ctx.properties.valueUnits});\nctx.api.font(element, valueTextFont, valueTextColor);\nctx.api.text(element, valueText);", "actions": null } ], @@ -432,13 +432,11 @@ "required": true, "subLabel": "Min", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, "rowClass": "column-xs", - "fieldClass": "", "min": 0, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxTemperature", @@ -447,14 +445,10 @@ "default": 45, "required": true, "subLabel": "Max", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "temperatureStep", @@ -462,31 +456,17 @@ "type": "number", "default": 1, "required": true, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 0.5 + "step": 0.5, + "disabled": false, + "visible": true }, { "id": "valueTextColor", "name": "{i18n:scada.symbol.value-text}", "type": "color", "default": "#000000C2", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueTextFont", @@ -499,192 +479,101 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueUnits", "name": "{i18n:scada.symbol.value-units}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.value-units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "valueBoxBackground", "name": "{i18n:scada.symbol.value-box-background}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "powerButtonBackground", "name": "{i18n:scada.symbol.power-button-background}", "type": "color", "default": "#1ABB48", - "required": null, "subLabel": "Enabled", "divider": true, - "fieldSuffix": null, - "disableOnProperty": null, "rowClass": "column-xs", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "disabledPowerButtonBackground", "name": "{i18n:scada.symbol.power-button-background}", "type": "color", "default": "#FFFFFF", - "required": null, "subLabel": "Disabled", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "runningColor", "name": "{i18n:scada.symbol.running-color}", "type": "color", "default": "#1C943E", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "stoppedColor", "name": "{i18n:scada.symbol.stopped-color}", "type": "color", "default": "#696969", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningColor", "name": "{i18n:scada.symbol.warning-color}", "type": "color", "default": "#FAA405", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalColor", "name": "{i18n:scada.symbol.critical-color}", "type": "color", "default": "#D12730", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "heatPumpColor", "name": "{i18n:scada.symbol.heat-pump-color}", "type": "color", "default": "#E5E5E5", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg index cd15a16577..d078450c9c 100644 --- a/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/short-vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 594 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(170, y, 202, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 160, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 160, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 160, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(182, minorY, 202, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -346,6 +346,22 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": false, + "supportsUnitConversion": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -359,16 +375,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg index 1b91a0cd4a..9425ed48f2 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg @@ -43,7 +43,7 @@ }, { "tag": "maxValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -53,7 +53,7 @@ }, { "tag": "minValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -73,12 +73,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -193,16 +193,8 @@ "name": "{i18n:scada.symbol.min-max-value}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minValue", @@ -211,14 +203,9 @@ "default": 0, "required": true, "subLabel": "{i18n:scada.symbol.min-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxValue", @@ -227,158 +214,89 @@ "default": 100, "required": true, "subLabel": "{i18n:scada.symbol.max-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showHighCriticalScale", "name": "{i18n:scada.symbol.high-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "highCriticalScale", "name": "{i18n:scada.symbol.high-critical-scale}", "type": "number", "default": 85, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showHighCriticalScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showHighWarningScale", "name": "{i18n:scada.symbol.high-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "highWarningScale", "name": "{i18n:scada.symbol.high-warning-scale}", "type": "number", "default": 70, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showHighWarningScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showLowWarningScale", "name": "{i18n:scada.symbol.low-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "lowWarningScale", "name": "{i18n:scada.symbol.low-warning-scale}", "type": "number", "default": 30, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showLowWarningScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showLowCriticalScale", "name": "{i18n:scada.symbol.low-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "lowCriticalScale", "name": "{i18n:scada.symbol.low-critical-scale}", "type": "number", "default": 15, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showLowCriticalScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "value", "name": "{i18n:scada.symbol.value}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueDecimals", @@ -387,14 +305,12 @@ "default": 0, "required": true, "subLabel": "Decimals", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", "min": 0, "max": 10, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "valueFont", @@ -407,64 +323,36 @@ "weight": "400", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueColor", "name": "{i18n:scada.symbol.value}", "type": "color", "default": "#002878", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "label", "name": "{i18n:scada.symbol.label}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelText", "name": "{i18n:scada.symbol.label}", "type": "text", "default": "Outdoor", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", "fieldClass": "flex", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelFont", @@ -477,64 +365,35 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelColor", "name": "{i18n:scada.symbol.label}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showUnits", "name": "{i18n:scada.symbol.units}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "unitsFont", @@ -547,160 +406,87 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "unitsColor", "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "arrowColor", "name": "{i18n:scada.symbol.arrow-color}", "type": "color", "default": "#666666", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showTarget", "name": "{i18n:scada.symbol.target}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "targetColor", "name": "{i18n:scada.symbol.target}", "type": "color", "default": "#DEDEDE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showTarget", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scaleColor", "name": "{i18n:scada.symbol.scale-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#EBEBEB", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#666666", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg index 7b8e7299b8..4e14130ef3 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg @@ -43,7 +43,7 @@ }, { "tag": "maxValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.maxValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -53,7 +53,7 @@ }, { "tag": "minValue", - "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.properties.minValue);\n}", + "stateRenderFunction": "if (ctx.properties.minMaxValue) {\n ctx.api.text(element, ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.units).toFixed(0));\n}", "actions": null }, { @@ -73,12 +73,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, null, ctx.properties.valueDecimals));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.value) {\n element.show();\n ctx.api.font(element, ctx.properties.valueFont, ctx.properties.valueColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.value, {units: ctx.properties.units, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -193,16 +193,8 @@ "name": "{i18n:scada.symbol.min-max-value}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "minValue", @@ -211,14 +203,9 @@ "default": 0, "required": true, "subLabel": "{i18n:scada.symbol.min-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "maxValue", @@ -227,158 +214,89 @@ "default": 100, "required": true, "subLabel": "{i18n:scada.symbol.max-value}", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showHighCriticalScale", "name": "{i18n:scada.symbol.high-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "highCriticalScale", "name": "{i18n:scada.symbol.high-critical-scale}", "type": "number", "default": 85, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showHighCriticalScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showHighWarningScale", "name": "{i18n:scada.symbol.high-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "highWarningScale", "name": "{i18n:scada.symbol.high-warning-scale}", "type": "number", "default": 70, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showHighWarningScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showLowWarningScale", "name": "{i18n:scada.symbol.low-warning-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "lowWarningScale", "name": "{i18n:scada.symbol.low-warning-scale}", "type": "number", "default": 30, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showLowWarningScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "showLowCriticalScale", "name": "{i18n:scada.symbol.low-critical-scale}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "lowCriticalScale", "name": "{i18n:scada.symbol.low-critical-scale}", "type": "number", "default": 15, - "required": null, - "subLabel": "", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showLowCriticalScale", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "value", "name": "{i18n:scada.symbol.value}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueDecimals", @@ -387,14 +305,12 @@ "default": 0, "required": true, "subLabel": "Decimals", - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", "min": 0, "max": 10, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "valueFont", @@ -407,64 +323,36 @@ "weight": "400", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "valueColor", "name": "{i18n:scada.symbol.value}", "type": "color", "default": "#002878", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "value", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "label", "name": "{i18n:scada.symbol.label}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelText", "name": "{i18n:scada.symbol.label}", "type": "text", "default": "Outdoor", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", "fieldClass": "flex", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelFont", @@ -477,64 +365,35 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "labelColor", "name": "{i18n:scada.symbol.label}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "label", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showUnits", "name": "{i18n:scada.symbol.units}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "units", "name": "{i18n:scada.symbol.units}", "type": "units", "default": "°C", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "supportsUnitConversion": true, + "disabled": false, + "visible": true }, { "id": "unitsFont", @@ -547,160 +406,87 @@ "weight": "500", "style": "normal" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "unitsColor", "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000000DE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showUnits", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "arrowColor", "name": "{i18n:scada.symbol.arrow-color}", "type": "color", "default": "#666666", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "showTarget", "name": "{i18n:scada.symbol.target}", "type": "switch", "default": false, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "targetColor", "name": "{i18n:scada.symbol.target}", "type": "color", "default": "#DEDEDE", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, "disableOnProperty": "showTarget", - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": true, + "visible": true }, { "id": "scaleColor", "name": "{i18n:scada.symbol.scale-color}", "type": "color", "default": "#C8DFF7", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#EBEBEB", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeWarningScaleColor", "name": "{i18n:scada.symbol.warning-scale-color}", "type": "color", "default": "#FAA405", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "defaultCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#666666", - "required": null, "subLabel": "Default", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeCriticalScaleColor", "name": "{i18n:scada.symbol.critical-scale-color}", "type": "color", "default": "#D12730", - "required": null, "subLabel": "Active", - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index 1d6785d0c1..95c4fd3eb5 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 45;\n var majorIntervalLength = 525 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -304,7 +304,7 @@ "type": "color", "default": "#F3F3F3", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -313,8 +313,8 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -329,7 +329,7 @@ "style": "normal" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -338,7 +338,7 @@ "type": "color", "default": "#0000008A", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg index 129006ffd7..3d5d6fdcb9 100644 --- a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg @@ -51,7 +51,7 @@ }, { "tag": "scale", - "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(50, end+11, 50, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(38, y, 50, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 32, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(44, minorY, 50, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(50, end+11, 50, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(38, y, 50, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 32, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(44, minorY, 50, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -433,7 +433,7 @@ "type": "units", "default": "%", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -493,6 +493,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -510,7 +518,7 @@ "name": "{i18n:scada.symbol.major-ticks}", "type": "font", "default": { - "size": 12, + "size": 10, "sizeUnit": "px", "family": "Roboto", "weight": "500", diff --git a/application/src/main/data/json/system/scada_symbols/small-meter.svg b/application/src/main/data/json/system/scada_symbols/small-meter.svg index 551c8d520a..a639475227 100644 --- a/application/src/main/data/json/system/scada_symbols/small-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/small-meter.svg @@ -51,7 +51,7 @@ }, { "tag": "scale", - "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 132 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 132 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(63, end+11, 63, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(51, y, 63, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 45, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(57, minorY, 63, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -424,7 +424,7 @@ "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -433,8 +433,8 @@ "type": "units", "default": "%", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -449,7 +449,7 @@ "style": "normal" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -490,7 +490,15 @@ "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, + "visible": true + }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, "visible": true }, { @@ -510,14 +518,12 @@ "name": "{i18n:scada.symbol.major-ticks}", "type": "font", "default": { - "size": 12, + "size": 10, "sizeUnit": "px", "family": "Roboto", "weight": "500", "style": "normal" - }, - "disabled": false, - "visible": true + } }, { "id": "majorColor", diff --git a/application/src/main/data/json/system/scada_symbols/small-right-center.svg b/application/src/main/data/json/system/scada_symbols/small-right-center.svg index de1bee5acd..8afe7bc88e 100644 --- a/application/src/main/data/json/system/scada_symbols/small-right-center.svg +++ b/application/src/main/data/json/system/scada_symbols/small-right-center.svg @@ -51,7 +51,7 @@ }, { "tag": "scale", - "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.properties.minValue;\n var maxValue = ctx.properties.maxValue;\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(73, end+11, 73, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(61, y, 73, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 55, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(67, minorY, 73, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "var scaleSet = element.remember('scaleSet');\nif (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n var minValue = ctx.api.convertUnitValue(ctx.properties.minValue, ctx.properties.valueUnits);\n var maxValue = ctx.api.convertUnitValue(ctx.properties.maxValue, ctx.properties.valueUnits);\n \n var start = 11;\n var end = ctx.properties.valueBox ? 134 : 167;\n var majorIntervalLength = end / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n element.add(ctx.svg.line(73, end+11, 73, 11).stroke({ width: 1 }).attr({class: 'majorTick'}));\n for (var i = 0; i < majorIntervals+1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(61, y, 73, y).stroke({ width: 1 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = (maxValue - ((maxValue - (minValue)) / (majorIntervals) * i)).toFixed(0);\n if (ctx.properties.enableUnitScale) {\n majorText = majorText + ctx.api.unitSymbol(ctx.properties.valueUnits);\n }\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 55, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n}\n\nvar majorFont = ctx.properties.majorFont;\nvar majorColor = ctx.properties.majorColor;\nvar minorColor = ctx.properties.minorColor;\nif (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n} else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n}\n\nvar majorTicks = element.find('line.majorTick');\nmajorTicks.forEach(t => t.attr({stroke: majorColor}));\n\nvar majorTicksText = element.find('text.majorTickText');\nctx.api.font(majorTicksText, majorFont, majorColor);\n\nvar minorTicks = element.find('line.minorTick');\nminorTicks.forEach(t => t.attr({stroke: minorColor}));\n\nvar elementCriticalAnimation = element.remember('criticalAnimation');\nvar criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\nif (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(67, minorY, 73, minorY).stroke({ width: 1 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -375,7 +375,7 @@ "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" }, "disableOnProperty": "progressArrow", - "disabled": false, + "disabled": true, "visible": true }, { @@ -424,7 +424,7 @@ "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -433,8 +433,8 @@ "type": "units", "default": "%", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -449,7 +449,7 @@ "style": "normal" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -490,7 +490,15 @@ "colorFunction": "var temperature = value;\nif (typeof temperature !== undefined) {\n var percent = (temperature + 60)/120 * 100;\n return tinycolor.mix('blue', 'red', percent).toHexString();\n}\nreturn 'blue';" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, + "visible": true + }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, "visible": true }, { @@ -510,7 +518,7 @@ "name": "{i18n:scada.symbol.major-ticks}", "type": "font", "default": { - "size": 12, + "size": 10, "sizeUnit": "px", "family": "Roboto", "weight": "500", diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index b8e3987188..8b0a1a20b0 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 560 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(268, y, 300, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 258, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(280, minorY, 300, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -304,7 +304,7 @@ "type": "color", "default": "#F3F3F3", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -313,8 +313,8 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -329,7 +329,7 @@ "style": "normal" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -338,7 +338,7 @@ "type": "color", "default": "#0000008A", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index f5d679fa96..44cd98e6f9 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 23;\n var majorIntervalLength = 960 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(458, y, 490, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 448, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(470, minorY, 490, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -304,7 +304,7 @@ "type": "color", "default": "#F3F3F3", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -313,8 +313,8 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", - "disabled": true, + "supportsUnitConversion": true, + "disabled": false, "visible": true }, { @@ -329,7 +329,7 @@ "style": "normal" }, "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -338,7 +338,7 @@ "type": "color", "default": "#0000008A", "disableOnProperty": "valueBox", - "disabled": true, + "disabled": false, "visible": true }, { @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index f5b8e8a892..9666f987d7 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 63a21bcf08..03995acbd5 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 17;\n var majorIntervalLength = 568 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(715, y, 747, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 705, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(727, minorY, 747, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index 0d56901bfe..b448d24463 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -35,7 +35,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -314,7 +314,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -370,6 +370,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -392,16 +400,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index 0d0e368b9b..c4dcc662fc 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -365,7 +365,17 @@ "value": false, "label": "Absolute" } - ] + ], + "disabled": false, + "visible": true + }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true }, { "id": "transparent", @@ -389,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", 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 index f2b7bf4d54..526f6aa719 100644 --- 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 @@ -38,7 +38,7 @@ }, { "tag": "night-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -48,7 +48,7 @@ }, { "tag": "off-peak-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.offPeakValueFont, ctx.properties.offPeakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.offPeakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -58,12 +58,12 @@ }, { "tag": "peak-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.peakValueFont, ctx.properties.peakValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.peakRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { @@ -480,6 +480,7 @@ "group": "{i18n:scada.symbol.off-peak-rate}", "type": "text", "default": "T1", + "disableOnProperty": "showOffPeakLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -496,6 +497,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showOffPeakLabel", "disabled": false, "visible": true }, @@ -505,6 +507,7 @@ "group": "{i18n:scada.symbol.off-peak-rate}", "type": "color", "default": "#000000", + "disableOnProperty": "showOffPeakLabel", "disabled": false, "visible": true }, @@ -556,6 +559,7 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "text", "default": "T2", + "disableOnProperty": "showNightLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -572,6 +576,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showNightLabel", "disabled": false, "visible": true }, @@ -581,6 +586,7 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "color", "default": "#000", + "disableOnProperty": "showNightLabel", "disabled": false, "visible": true }, @@ -632,6 +638,7 @@ "group": "{i18n:scada.symbol.peak-rate}", "type": "text", "default": "T3", + "disableOnProperty": "showPeakLabel", "fieldClass": "medium-width", "disabled": false, "visible": true @@ -648,6 +655,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showPeakLabel", "disabled": false, "visible": true }, @@ -657,6 +665,7 @@ "group": "{i18n:scada.symbol.peak-rate}", "type": "color", "default": "#000", + "disableOnProperty": "showPeakLabel", "disabled": false, "visible": true }, @@ -706,6 +715,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "kWh", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -720,6 +730,7 @@ "weight": "normal", "style": "normal" }, + "disableOnProperty": "showUnits", "disabled": false, "visible": true }, @@ -728,6 +739,7 @@ "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000", + "disableOnProperty": "showUnits", "disabled": false, "visible": true } diff --git a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg index 9c09a6067e..ae9b6701af 100644 --- a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -464,8 +464,7 @@ "type": "units", "default": "m³/hr", "fieldClass": "medium-width", - "disabled": false, - "visible": true + "supportsUnitConversion": true }, { "id": "valueDecimals", 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 index 7adc401836..e87548f059 100644 --- 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 @@ -38,7 +38,7 @@ }, { "tag": "day-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.dayValueFont, ctx.properties.dayValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.dayRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.dayValueFont, ctx.properties.dayValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.dayRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { @@ -48,12 +48,12 @@ }, { "tag": "night-rate", - "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, 0, null, 0));", + "stateRenderFunction": "ctx.api.font(element, ctx.properties.nightValueFont, ctx.properties.nightValueColor);\nctx.api.text(element, ctx.api.formatValue(ctx.values.nightRate, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));", "actions": null }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { @@ -427,8 +427,9 @@ "group": "{i18n:scada.symbol.day-rate}", "type": "text", "default": "T1", + "disableOnProperty": "showDayLabel", "fieldClass": "medium-width", - "disabled": false, + "disabled": true, "visible": true }, { @@ -443,7 +444,8 @@ "weight": "normal", "style": "normal" }, - "disabled": false, + "disableOnProperty": "showDayLabel", + "disabled": true, "visible": true }, { @@ -452,7 +454,8 @@ "group": "{i18n:scada.symbol.day-rate}", "type": "color", "default": "#000000", - "disabled": false, + "disableOnProperty": "showDayLabel", + "disabled": true, "visible": true }, { @@ -503,8 +506,9 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "text", "default": "T2", + "disableOnProperty": "showNightLabel", "fieldClass": "medium-width", - "disabled": false, + "disabled": true, "visible": true }, { @@ -519,7 +523,8 @@ "weight": "normal", "style": "normal" }, - "disabled": false, + "disableOnProperty": "showNightLabel", + "disabled": true, "visible": true }, { @@ -528,7 +533,8 @@ "group": "{i18n:scada.symbol.night-rate}", "type": "color", "default": "#000", - "disabled": false, + "disableOnProperty": "showNightLabel", + "disabled": true, "visible": true }, { @@ -577,6 +583,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "kWh", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -591,7 +598,8 @@ "weight": "normal", "style": "normal" }, - "disabled": false, + "disableOnProperty": "showUnits", + "disabled": true, "visible": true }, { @@ -599,7 +607,8 @@ "name": "{i18n:scada.symbol.units}", "type": "color", "default": "#000", - "disabled": false, + "disableOnProperty": "showUnits", + "disabled": true, "visible": true } ] diff --git a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg index 8f9a30e576..e270024f8b 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, {units: ctx.properties.valueUnits, decimals: ctx.properties.valueDecimals, ignoreUnitSymbol: true});\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -66,7 +66,7 @@ }, { "tag": "valueUnits", - "stateRenderFunction": "var units = ctx.properties.valueUnits;\nctx.api.text(element, units || '');\n", + "stateRenderFunction": "var units = ctx.api.unitSymbol(ctx.properties.valueUnits);\nctx.api.text(element, units || '');\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -464,6 +464,7 @@ "type": "units", "default": "m³/hr", "fieldClass": "medium-width", + "supportsUnitConversion": true, "disabled": false, "visible": true }, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index 57a79d72d7..5d8aa42ab5 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -34,7 +34,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 137;\n var majorIntervalLength = 442 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(523, y, 555, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 513, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(535, minorY, 555, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -313,7 +313,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,17 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "required": false, - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg index 9dee6cc2af..d7f0cb11d9 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank-hp.svg @@ -38,7 +38,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 3;\n var majorIntervalLength = 994 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(160, y, 192, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.majorUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n if (i === 0) {\n majorTickText.attr({x: 150, y: y + 10, 'text-anchor': 'end', class: 'majorTickText'});\n } else if (i === majorIntervals) {\n majorTickText.attr({x: 150, y: y - 5, 'text-anchor': 'end', class: 'majorTickText'});\n } else {\n majorTickText.attr({x: 150, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n }\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(172, minorY, 192, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -346,6 +346,24 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, + { + "id": "majorUnits", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "units", + "subLabel": "{i18n:scada.symbol.units}", + "divider": false, + "supportsUnitConversion": true, + "disabled": false, + "visible": true + }, { "id": "majorIntervals", "name": "{i18n:scada.symbol.major-ticks}", @@ -359,16 +377,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index 99fa9b649c..5e51330ded 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -33,7 +33,7 @@ }, { "tag": "scale", - "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var majorText = ctx.api.formatValue((tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0), 0, ctx.properties.majorUnits, false);\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", + "stateRenderFunction": "if (!ctx.properties.scale) {\n element.hide();\n} else {\n var scaleSet = element.remember('scaleSet');\n if (!scaleSet) {\n element.remember('scaleSet', true);\n element.clear();\n \n var majorIntervals = ctx.properties.majorIntervals;\n var minorIntervals = ctx.properties.minorIntervals;\n \n var start = 205;\n var majorIntervalLength = 760 / majorIntervals;\n var minorIntervalLength = majorIntervalLength / minorIntervals;\n var tankCapacity = ctx.properties.scaleDisplayFormat ? 100 : (ctx.values.tankCapacity || 100);\n for (var i = 0; i < majorIntervals + 1; i++) {\n var y = start + i * majorIntervalLength;\n var line = ctx.svg.line(340, y, 372, y).stroke({ width: 3 }).attr({class: 'majorTick'});\n element.add(line);\n var currentVolume = (tankCapacity - i * (tankCapacity/majorIntervals)).toFixed(0);\n var majorText = ctx.properties.scaleDisplayFormat ? currentVolume : ctx.api.formatValue(currentVolume, {units: ctx.properties.valueUnits, decimals: 0, ignoreUnitSymbol: !ctx.properties.enableUnitScale});\n var majorTickText = ctx.svg.text(majorText);\n majorTickText.attr({x: 330, y: y + 2, 'text-anchor': 'end', class: 'majorTickText'});\n majorTickText.first().attr({'dominant-baseline': 'middle'});\n element.add(majorTickText);\n if (i < majorIntervals) {\n drawMinorTicks(y, minorIntervals, minorIntervalLength);\n }\n }\n }\n \n var majorFont = ctx.properties.majorFont;\n var majorColor = ctx.properties.majorColor;\n var minorColor = ctx.properties.minorColor;\n if (ctx.values.critical) {\n majorColor = ctx.properties.majorCriticalColor;\n minorColor = ctx.properties.minorCriticalColor;\n } else if (ctx.values.warning) {\n majorColor = ctx.properties.minorWarningColor;\n minorColor = ctx.properties.minorWarningColor;\n }\n \n var majorTicks = element.find('line.majorTick');\n majorTicks.forEach(t => t.attr({stroke: majorColor}));\n \n var majorTicksText = element.find('text.majorTickText');\n ctx.api.font(majorTicksText, majorFont, majorColor);\n \n var minorTicks = element.find('line.minorTick');\n minorTicks.forEach(t => t.attr({stroke: minorColor}));\n \n var elementCriticalAnimation = element.remember('criticalAnimation');\n var criticalAnimation = ctx.values.critical && ctx.values.criticalAnimation;\n\n if (elementCriticalAnimation !== criticalAnimation) {\n element.remember('criticalAnimation', criticalAnimation);\n if (criticalAnimation) {\n ctx.api.cssAnimate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetCssAnimation(element);\n }\n }\n}\n\nfunction drawMinorTicks(start, minorIntervals, minorIntervalLength) {\n for (var i = 1; i < minorIntervals; i++) {\n var minorY = start + i * minorIntervalLength;\n var minorLine = ctx.svg.line(352, minorY, 372, minorY).stroke({ width: 3 }).attr({class: 'minorTick'});\n element.add(minorLine);\n }\n}", "actions": null }, { @@ -312,7 +312,7 @@ "type": "units", "default": "gal", "subLabel": "{i18n:scada.symbol.units}", - "disableOnProperty": "valueBox", + "supportsUnitConversion": true, "disabled": false, "visible": true }, @@ -369,6 +369,14 @@ "disabled": false, "visible": true }, + { + "id": "enableUnitScale", + "name": "{i18n:scada.symbol.enable-units-scale}", + "type": "switch", + "default": false, + "disabled": false, + "visible": true + }, { "id": "transparent", "name": "{i18n:scada.symbol.transparent-mode}", @@ -391,16 +399,6 @@ "disabled": false, "visible": true }, - { - "id": "majorUnits", - "name": "{i18n:scada.symbol.major-ticks}", - "type": "units", - "subLabel": "{i18n:scada.symbol.units}", - "divider": true, - "disableOnProperty": "scale", - "disabled": false, - "visible": true - }, { "id": "majorFont", "name": "{i18n:scada.symbol.major-ticks}", 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 index eaa0b02455..fa27214864 100644 --- 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 @@ -34,12 +34,12 @@ }, { "tag": "units", - "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.properties.units);\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.properties.showUnits) {\n element.show();\n ctx.api.font(element, ctx.properties.unitsFont, ctx.properties.unitsColor);\n ctx.api.text(element, ctx.api.unitSymbol(ctx.properties.units));\n} else {\n element.hide();\n}", "actions": null }, { "tag": "value", - "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.voltage, 0, null, 0));\n} else {\n element.hide();\n}", + "stateRenderFunction": "if (ctx.values.running) {\n element.show();\n ctx.api.font(element, ctx.properties.currentVoltageFont, ctx.properties.currentVoltageColor);\n ctx.api.text(element, ctx.api.formatValue(ctx.values.voltage, {units: ctx.properties.units, decimals: 0, ignoreUnitSymbol: true}));\n} else {\n element.hide();\n}", "actions": null }, { @@ -379,6 +379,7 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "V", + "supportsUnitConversion": true, "disabled": false, "visible": true }, diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 2443037ca0..8daee6800b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -199,6 +199,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; @@ -420,7 +421,7 @@ public abstract class BaseController { return handleException(exception, true); } - private ThingsboardException handleException(Exception exception, boolean logException) { + private ThingsboardException handleException(Throwable exception, boolean logException) { if (logException && logControllerErrorStackTrace) { try { SecurityUser user = getCurrentUser(); @@ -431,6 +432,9 @@ public abstract class BaseController { } Throwable cause = exception.getCause(); + if (exception instanceof ExecutionException) { + exception = cause; + } if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 3257b31ca4..2dcb32cf39 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -244,7 +244,7 @@ public class CalculatedFieldController extends BaseController { ); Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLastUpdateTimestamp(arguments)); + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); for (int i = 1; i < ctxAndArgNames.size(); i++) { var arg = arguments.get(ctxAndArgNames.get(i)); if (arg instanceof TbelCfSingleValueArg svArg) { @@ -267,7 +267,7 @@ public class CalculatedFieldController extends BaseController { return result; } - private long getLastUpdateTimestamp(Map arguments) { + private long getLatestTimestamp(Map arguments) { long lastUpdateTimestamp = -1; for (TbelCfArg entry : arguments.values()) { if (entry instanceof TbelCfSingleValueArg singleValueArg) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java index eb86220361..f9ec8087ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldQueueService.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.RuleEngineCalculatedFieldQueueService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import java.util.List; @@ -35,7 +36,7 @@ public interface CalculatedFieldQueueService extends RuleEngineCalculatedFieldQu */ void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback); - void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback); + void pushRequestToQueue(AttributesSaveRequest request, AttributesSaveResult result, FutureCallback callback); void pushRequestToQueue(AttributesDeleteRequest request, List result, FutureCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index 0c2d1a24a9..a423eb6669 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -98,7 +99,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } @Override - public void pushRequestToQueue(AttributesSaveRequest request, List result, FutureCallback callback) { + public void pushRequestToQueue(AttributesSaveRequest request, AttributesSaveResult result, FutureCallback callback) { var tenantId = request.getTenantId(); var entityId = request.getEntityId(); var entries = request.getEntries(); @@ -209,15 +210,18 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS return ToCalculatedFieldMsg.newBuilder().setTelemetryMsg(telemetryMsg).build(); } - private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, List versions) { + private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(AttributesSaveRequest request, AttributesSaveResult result) { ToCalculatedFieldMsg.Builder msg = ToCalculatedFieldMsg.newBuilder(); CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType()); telemetryMsg.setScope(AttributeScopeProto.valueOf(request.getScope().name())); + List entries = request.getEntries(); + List versions = result.versions(); + for (int i = 0; i < entries.size(); i++) { AttributeValueProto.Builder attrProtoBuilder = ProtoUtils.toProto(entries.get(i)).toBuilder(); - if (versions != null) { + if (versions != null && !versions.isEmpty() && versions.get(i) != null) { attrProtoBuilder.setVersion(versions.get(i)); } telemetryMsg.addAttrData(attrProtoBuilder.build()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index e4b03b4cab..e21d56b6d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -35,7 +35,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected Map arguments; protected boolean sizeExceedsLimit; - protected long lastUpdateTimestamp = -1; + protected long latestTimestamp = -1; public BaseCalculatedFieldState(List requiredArguments) { this.requiredArguments = requiredArguments; @@ -110,12 +110,14 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected abstract void validateNewEntry(ArgumentEntry newEntry); private void updateLastUpdateTimestamp(ArgumentEntry entry) { + long newTs = this.latestTimestamp; if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - this.lastUpdateTimestamp = singleValueArgumentEntry.getTs(); + newTs = singleValueArgumentEntry.getTs(); } else if (entry instanceof TsRollingArgumentEntry tsRollingArgumentEntry) { Map.Entry lastEntry = tsRollingArgumentEntry.getTsRecords().lastEntry(); - this.lastUpdateTimestamp = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis(); + newTs = (lastEntry != null) ? lastEntry.getKey() : System.currentTimeMillis(); } + this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index b6c04426ba..0008874779 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -62,7 +62,7 @@ public class CalculatedFieldCtx { private final List argNames; private Output output; private String expression; - private boolean preserveMsgTs; + private boolean useLatestTs; private TbelInvokeService tbelInvokeService; private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private ThreadLocal customExpression; @@ -96,7 +96,7 @@ public class CalculatedFieldCtx { this.argNames = new ArrayList<>(arguments.keySet()); this.output = configuration.getOutput(); this.expression = configuration.getExpression(); - this.preserveMsgTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isPreserveMsgTs(); + this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs(); this.tbelInvokeService = tbelInvokeService; this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 6eac3358ba..0de354bbb0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -42,7 +42,7 @@ public interface CalculatedFieldState { Map getArguments(); - long getLastUpdateTimestamp(); + long getLatestTimestamp(); void setRequiredArguments(List requiredArguments); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 65ef40330c..84dce627ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -66,7 +66,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { args.add(arg); } } - args.set(0, new TbelCfCtx(arguments, getLastUpdateTimestamp())); + args.set(0, new TbelCfCtx(arguments, getLatestTimestamp())); ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); Output output = ctx.getOutput(); return Futures.transform(resultFuture, diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 03399348cb..c07923e470 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -79,7 +79,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { Output output = ctx.getOutput(); Object result = formatResult(expressionResult, output.getDecimalsByDefault()); - JsonNode outputResult = createResultJson(ctx.isPreserveMsgTs(), output.getName(), result); + JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); } @@ -94,7 +94,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { return TbUtils.toFixed(expressionResult, decimals); } - private JsonNode createResultJson(boolean preserveMsgTs, String outputName, Object result) { + private JsonNode createResultJson(boolean useLatestTs, String outputName, Object result) { ObjectNode valuesNode = JacksonUtil.newObjectNode(); if (result instanceof Double doubleValue) { valuesNode.put(outputName, doubleValue); @@ -104,10 +104,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } - long lastTimestamp = getLastUpdateTimestamp(); - if (preserveMsgTs && lastTimestamp != -1) { + long latestTs = getLatestTimestamp(); + if (useLatestTs && latestTs != -1) { ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", lastTimestamp); + resultNode.put("ts", latestTs); resultNode.set("values", valuesNode); return resultNode; } else { diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index 173ba742af..0778d61ee7 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -62,8 +63,6 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -240,10 +239,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); } - private ListenableFuture> saveProvisionStateAttribute(Device device) { - return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, - Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), - System.currentTimeMillis()))); + private ListenableFuture saveProvisionStateAttribute(Device device) { + return attributesService.save( + device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, + new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), System.currentTimeMillis()) + ); } private DeviceCredentials getDeviceCredentials(Device device) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 73da3694f9..5c95a0e98f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -69,7 +69,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -193,6 +195,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); + this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -262,6 +265,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @Override public void updateEdge(TenantId tenantId, Edge edge) { + if (edge == null) { + log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId); + return; + } EdgeGrpcSession session = sessions.get(edge.getId()); if (session != null && session.isConnected()) { log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); @@ -459,11 +466,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception { boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE); if (!isMigrationProcessed) { + log.info("Starting edge event migration for edge [{}]", edgeId.getId()); Boolean eventsExist = session.migrateEdgeEvents().get(); if (Boolean.TRUE.equals(eventsExist)) { + log.info("Migration still in progress for edge [{}]", edgeId.getId()); sessionNewEvents.put(edgeId, true); scheduleEdgeEventsCheck(session); } else if (Boolean.FALSE.equals(eventsExist)) { + log.info("Migration completed for edge [{}]", edgeId.getId()); edgeEventsMigrationProcessed.put(edgeId, true); } } @@ -610,4 +620,27 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } + private void destroyKafkaSessionIfDisconnectedAndConsumerActive() { + try { + List toRemove = new ArrayList<>(); + for (EdgeGrpcSession session : sessions.values()) { + if (session instanceof KafkaEdgeGrpcSession kafkaSession && + !kafkaSession.isConnected() && + kafkaSession.getConsumer() != null && + kafkaSession.getConsumer().getConsumer() != null && + !kafkaSession.getConsumer().getConsumer().isStopped()) { + toRemove.add(kafkaSession.getEdge().getId()); + } + } + for (EdgeId edgeId : toRemove) { + log.info("[{}] Destroying session for edge because edge is not connected", edgeId); + EdgeGrpcSession removed = sessions.remove(edgeId); + if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { + kafkaSession.destroy(); + } + } + } catch (Exception e) { + log.warn("Failed to cleanup kafka sessions", e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 4a9b68fc6d..b6ecd848ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; @@ -42,7 +43,6 @@ import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; @@ -292,11 +292,11 @@ public abstract class EdgeGrpcSession implements Closeable { protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) { try { - log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); + log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); processHighPriorityEvents(); PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { - log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, sessionId, pageData.getData().size()); + log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @Override @@ -323,16 +323,16 @@ public abstract class EdgeGrpcSession implements Closeable { @Override public void onFailure(Throwable t) { - log.error("[{}] Failed to send downlink msgs pack", sessionId, t); + log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { - log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); + log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink); result.set(null); } } catch (Exception e) { - log.error("[{}] Failed to fetch edge events", sessionId, e); + log.error("[{}] Failed to fetch edge events", edge.getId(), e); result.setException(e); } } @@ -459,9 +459,9 @@ public abstract class EdgeGrpcSession implements Closeable { ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); } - log.warn("[{}][{}] {}, attempt: {}", tenantId, sessionId, failureMsg, attempt); + log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt); } - log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, sessionId, copy.size()); + log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); for (DownlinkMsg downlinkMsg : copy) { if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + @@ -483,7 +483,7 @@ public abstract class EdgeGrpcSession implements Closeable { } else { String failureMsg = String.format("Failed to deliver messages: %s", copy); log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", - tenantId, sessionId, MAX_DOWNLINK_ATTEMPTS, copy); + tenantId, edge.getId(), MAX_DOWNLINK_ATTEMPTS, copy); ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); @@ -493,7 +493,7 @@ public abstract class EdgeGrpcSession implements Closeable { stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { - log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, sessionId, e.getMessage(), e); + log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, edge.getId(), e.getMessage(), e); stopCurrentSendDownlinkMsgsTask(true); } }; @@ -540,7 +540,7 @@ public abstract class EdgeGrpcSession implements Closeable { stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { - log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, sessionId, msg, e); + log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, edge.getId(), msg, e); } } @@ -555,12 +555,12 @@ public abstract class EdgeGrpcSession implements Closeable { while ((event = highPriorityQueue.poll()) != null) { highPriorityEvents.add(event); } - log.trace("[{}][{}] Sending high priority events {}", tenantId, sessionId, highPriorityEvents.size()); + log.trace("[{}][{}] Sending high priority events {}", tenantId, edge.getId(), highPriorityEvents.size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); sendDownlinkMsgsPack(downlinkMsgsPack).get(); } } catch (Exception e) { - log.error("[{}] Failed to process high priority events", sessionId, e); + log.error("[{}] Failed to process high priority events", edge.getId(), e); } } @@ -577,16 +577,16 @@ public abstract class EdgeGrpcSession implements Closeable { Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), ctx.getEdgeEventService()); log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", - tenantId, sessionId, previousStartTs, previousStartSeqId); + tenantId, edge.getId(), previousStartTs, previousStartSeqId); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { @Override public void onSuccess(@Nullable Pair newStartTsAndSeqId) { if (newStartTsAndSeqId != null) { - ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); + ListenableFuture updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); Futures.addCallback(updateFuture, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List list) { - log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); + public void onSuccess(@Nullable AttributesSaveResult saveResult) { + log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edge.getId(), newStartTsAndSeqId); boolean newEventsAvailable; if (fetcher.isSeqIdNewCycleStarted()) { newEventsAvailable = isNewEdgeEventsAvailable(); @@ -601,28 +601,28 @@ public abstract class EdgeGrpcSession implements Closeable { @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, sessionId, newStartTsAndSeqId, t); + log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edge.getId(), newStartTsAndSeqId, t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { - log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, sessionId); + log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edge.getId()); result.set(Boolean.FALSE); } } @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to process events", tenantId, sessionId, t); + log.error("[{}][{}] Failed to process events", tenantId, edge.getId(), t); result.setException(t); } }, ctx.getGrpcCallbackExecutorService()); } else { if (isSyncInProgress()) { - log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, sessionId); + log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edge.getId()); result.set(Boolean.TRUE); } else { - log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, sessionId); + log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edge.getId()); result.set(null); } } @@ -632,7 +632,7 @@ public abstract class EdgeGrpcSession implements Closeable { protected List convertToDownlinkMsgsPack(List edgeEvents) { List result = new ArrayList<>(); for (EdgeEvent edgeEvent : edgeEvents) { - log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, sessionId, edgeEvent); + log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent); DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { @@ -641,17 +641,16 @@ public abstract class EdgeGrpcSession implements Closeable { ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { - log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, sessionId, downlinkMsg.getDownlinkMsgId()); + log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, edge.getId(), downlinkMsg.getDownlinkMsgId()); } else { - log.trace("[{}][{}] entity message processed [{}]", tenantId, sessionId, downlinkMsg); + log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg); } } - case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> - downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); - default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction()); + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); + default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction()); } } catch (Exception e) { - log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); + log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, edge.getId(), e); } if (downlinkMsg != null) { result.add(downlinkMsg); @@ -723,13 +722,14 @@ public abstract class EdgeGrpcSession implements Closeable { return startSeqId; } - private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) { + private ListenableFuture updateQueueStartTsAndSeqId(Pair pair) { newStartTs = pair.getFirst(); newStartSeqId = pair.getSecond(); log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", sessionId, edge.getId(), newStartTs, newStartSeqId); - List attributes = Arrays.asList( + List attributes = List.of( new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()), - new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis())); + new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, newStartSeqId), System.currentTimeMillis()) + ); return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), AttributeScope.SERVER_SCOPE, attributes); } @@ -757,19 +757,19 @@ public abstract class EdgeGrpcSession implements Closeable { private void sendDownlinkMsg(ResponseMsg responseMsg) { if (isConnected()) { String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); - log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, sessionId, responseMsgStr); + log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, edge.getId(), responseMsgStr); downlinkMsgLock.lock(); String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; try { outputStream.onNext(responseMsg); } catch (Exception e) { - log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); + log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, edge.getId(), downlinkMsgStr, e); connected = false; sessionCloseListener.accept(edge, sessionId); } finally { downlinkMsgLock.unlock(); } - log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, sessionId, downlinkMsgStr); + log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, edge.getId(), downlinkMsgStr); } } @@ -909,8 +909,8 @@ public abstract class EdgeGrpcSession implements Closeable { } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); - log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); - ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) + log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); + ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); return Futures.immediateFailedFuture(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index 37687f14a9..daffe9db11 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import io.grpc.stub.StreamObserver; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.edge.Edge; @@ -56,6 +57,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { private volatile boolean isHighPriorityProcessing; + @Getter private QueueConsumerManager> consumer; private ExecutorService consumerExecutor; @@ -72,31 +74,28 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { } private void processMsgs(List> msgs, TbQueueConsumer> consumer) { - log.trace("[{}][{}] starting processing edge events", tenantId, sessionId); - if (isConnected() && !isSyncInProgress() && !isHighPriorityProcessing) { - List edgeEvents = new ArrayList<>(); - for (TbProtoQueueMsg msg : msgs) { - EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); - edgeEvents.add(edgeEvent); - } - List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); - try { - boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); - if (isInterrupted) { - log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); - } else { - consumer.commit(); - } - } catch (Exception e) { - log.error("[{}] Failed to process all downlink messages", sessionId, e); - } - } else { - try { - Thread.sleep(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()); - } catch (InterruptedException interruptedException) { - log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); + log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId()); + if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { + log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + + "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", + tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing); + return; + } + List edgeEvents = new ArrayList<>(); + for (TbProtoQueueMsg msg : msgs) { + EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); + edgeEvents.add(edgeEvent); + } + List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); + try { + boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); + if (isInterrupted) { + log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId()); + } else { + consumer.commit(); } - log.trace("[{}][{}] edge is not connected or sync is not completed. Skipping iteration", tenantId, sessionId); + } catch (Exception e) { + log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e); } } @@ -107,18 +106,23 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { @Override public ListenableFuture processEdgeEvents() { - if (consumer == null) { - this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); - this.consumer = QueueConsumerManager.>builder() - .name("TB Edge events") - .msgPackProcessor(this::processMsgs) - .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) - .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) - .consumerExecutor(consumerExecutor) - .threadPrefix("edge-events") - .build(); - consumer.subscribe(); - consumer.launch(); + if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) { + try { + this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); + this.consumer = QueueConsumerManager.>builder() + .name("TB Edge events [" + edge.getId() + "]") + .msgPackProcessor(this::processMsgs) + .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) + .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) + .consumerExecutor(consumerExecutor) + .threadPrefix("edge-events-" + edge.getId()) + .build(); + consumer.subscribe(); + consumer.launch(); + } catch (Exception e) { + destroy(); + log.warn("[{}][{}] Failed to start edge event consumer", sessionId, edge.getId(), e); + } } return Futures.immediateFuture(Boolean.FALSE); } @@ -132,8 +136,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { @Override public void destroy() { - consumer.stop(); - consumerExecutor.shutdown(); + try { + if (consumer != null) { + consumer.stop(); + } + } finally { + consumer = null; + } + try { + if (consumerExecutor != null) { + consumerExecutor.shutdown(); + } + } catch (Exception ignored) {} } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index e9ef8c5ace..d580175aa0 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -581,9 +582,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); } else { - ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, - Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) - , System.currentTimeMillis()))); + ListenableFuture saveFuture = attributesService.save( + TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, new BaseAttributeKvEntry(new BooleanDataEntry(key, value), System.currentTimeMillis()) + ); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); } } @@ -611,7 +612,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } private void addTsCallback(ListenableFuture saveFuture, final FutureCallback callback) { - Futures.addCallback(saveFuture, new FutureCallback() { + Futures.addCallback(saveFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable S result) { callback.onSuccess(result); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index 62455117e0..d6cee80ebf 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -66,8 +67,8 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess private final NotificationDeduplicationService deduplicationService; private final PartitionService partitionService; private final RateLimitService rateLimitService; - @Autowired @Lazy - private NotificationCenter notificationCenter; + @Lazy + private final NotificationCenter notificationCenter; private final NotificationExecutorService notificationExecutor; private final Map triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); @@ -82,14 +83,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess if (enabledRules.isEmpty()) { return; } - if (trigger.deduplicate()) { - enabledRules = new ArrayList<>(enabledRules); - enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule)); - } - final List rules = enabledRules; - for (NotificationRule rule : rules) { + + List rulesToProcess = filterNotificationRules(trigger, enabledRules); + for (NotificationRule rule : rulesToProcess) { try { - processNotificationRule(rule, trigger); + processNotificationRule(rule, trigger, DeduplicationStrategy.ONLY_MATCHING.equals(trigger.getDeduplicationStrategy())); } catch (Throwable e) { log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); } @@ -100,7 +98,20 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess }); } - private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger) { + private List filterNotificationRules(NotificationRuleTrigger trigger, List enabledRules) { + List rulesToProcess = new ArrayList<>(enabledRules); + rulesToProcess.removeIf(rule -> switch (trigger.getDeduplicationStrategy()) { + case ONLY_MATCHING -> { + boolean matched = matchesFilter(trigger, rule.getTriggerConfig()); + yield !matched || deduplicationService.alreadyProcessed(trigger, rule); + } + case ALL -> deduplicationService.alreadyProcessed(trigger, rule); + default -> false; + }); + return rulesToProcess; + } + + private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger, boolean alreadyMatched) { NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); @@ -114,7 +125,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess return; } - if (matchesFilter(trigger, triggerConfig)) { + if (alreadyMatched || matchesFilter(trigger, triggerConfig)) { if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) { log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName()); return; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java index aefb628d2d..09c8d76eca 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java @@ -39,7 +39,12 @@ public class ResourcesShortageTriggerProcessor implements NotificationRuleTrigge @Override public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) { - return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build(); + return ResourcesShortageNotificationInfo.builder() + .resource(trigger.getResource().name()) + .usage(trigger.getUsage()) + .serviceId(trigger.getServiceId()) + .serviceType(trigger.getServiceType()) + .build(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index cc476d377d..23fdc6b8c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -27,11 +27,10 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; +import org.checkerframework.checker.nullness.qual.NonNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; @@ -170,35 +169,22 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService> stats = new HashMap<>(); for (DeviceStateData stateData : deviceStates.values()) { @@ -574,7 +559,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService() { + @Override + public void onSuccess(Void success) { + stateData.getState().setLastInactivityAlarmTime(ts); + onDeviceActivityStatusChange(false, stateData); + } + + @Override + public void onFailure(@NonNull Throwable t) { + log.error("[{}][{}] Failed to update device last inactivity alarm time to '{}'. Device state data: {}", tenantId, deviceId, ts, stateData, t); + } + }, deviceStateCallbackExecutor); } - boolean isActive(long ts, DeviceState state) { + private static boolean isActive(long ts, DeviceState state) { return ts < state.getLastActivityTime() + state.getInactivityTimeout(); } @@ -616,17 +611,32 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService() { + @Override + public void onSuccess(Void success) { + stateData.getState().setActive(active); + pushRuleEngineMessage(stateData, active ? TbMsgType.ACTIVITY_EVENT : TbMsgType.INACTIVITY_EVENT); + TbMsgMetaData metaData = stateData.getMetaData(); + notificationRuleProcessor.process(DeviceActivityTrigger.builder() + .tenantId(tenantId) + .customerId(stateData.getCustomerId()) + .deviceId(deviceId) + .active(active) + .deviceName(metaData.getValue("deviceName")) + .deviceType(metaData.getValue("deviceType")) + .deviceLabel(metaData.getValue("deviceLabel")) + .build()); + } + + @Override + public void onFailure(@NonNull Throwable t) { + log.error("[{}][{}] Failed to change device activity status to '{}'. Device state data: {}", tenantId, deviceId, active, stateData, t); + } + }, deviceStateCallbackExecutor); } boolean cleanDeviceStateIfBelongsToExternalPartition(TenantId tenantId, final DeviceId deviceId) { @@ -634,8 +644,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService save(TenantId tenantId, DeviceId deviceId, String key, long value) { + return save(tenantId, deviceId, new LongDataEntry(key, value), getCurrentTimeMillis()); } - private void save(TenantId tenantId, DeviceId deviceId, String key, boolean value) { - save(tenantId, deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis()); + private ListenableFuture save(TenantId tenantId, DeviceId deviceId, String key, boolean value) { + return save(tenantId, deviceId, new BooleanDataEntry(key, value), getCurrentTimeMillis()); } - private void save(TenantId tenantId, DeviceId deviceId, KvEntry kvEntry, long ts) { + private ListenableFuture save(TenantId tenantId, DeviceId deviceId, KvEntry kvEntry, long ts) { + ListenableFuture future; if (persistToTelemetry) { - tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() + future = tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() .tenantId(tenantId) .entityId(deviceId) .entry(new BasicTsKvEntry(ts, kvEntry)) @@ -895,7 +908,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, kvEntry)) .build()); } else { - tsSubService.saveAttributes(AttributesSaveRequest.builder() + future = tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() .tenantId(tenantId) .entityId(deviceId) .scope(AttributeScope.SERVER_SCOPE) @@ -903,20 +916,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, kvEntry)) .build()); } + return Futures.transform(future, __ -> null, MoreExecutors.directExecutor()); } long getCurrentTimeMillis() { return System.currentTimeMillis(); } - private static class TelemetrySaveCallback implements FutureCallback { - private final DeviceId deviceId; - private final KvEntry kvEntry; - - TelemetrySaveCallback(DeviceId deviceId, KvEntry kvEntry) { - this.deviceId = deviceId; - this.kvEntry = kvEntry; - } + private record TelemetrySaveCallback(DeviceId deviceId, KvEntry kvEntry) implements FutureCallback { @Override public void onSuccess(@Nullable T result) { @@ -924,9 +931,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); clusterSystemData.forEach(data -> { - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build()); - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build()); - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build()); + Arrays.stream(Resource.values()).forEach(resource -> { + notificationRuleProcessor.process(ResourcesShortageTrigger.builder() + .resource(resource) + .serviceId(data.getServiceId()) + .serviceType(data.getServiceType()) + .usage(extractResourceUsage(data, resource)) + .build()); + }); }); BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); @@ -200,17 +205,17 @@ public class DefaultSystemInfoService extends TbApplicationEventListener { long value = (long) v; tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value))); - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build()); + notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build()); }); getMemoryUsage().ifPresent(v -> { long value = (long) v; tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value))); - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build()); + notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build()); }); getDiscSpaceUsage().ifPresent(v -> { long value = (long) v; tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value))); - notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build()); + notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build()); }); getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v)))); @@ -258,6 +263,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener info.getCpuUsage(); + case RAM -> info.getMemoryUsage(); + case STORAGE -> info.getDiscUsage(); + }; + } + @PreDestroy private void destroy() { if (scheduler != null) { 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 0f4f615af3..a14e052659 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 @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -62,7 +63,6 @@ import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -191,17 +191,16 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } @Override - public void saveAttributesInternal(AttributesSaveRequest request) { - log.trace("Executing saveInternal [{}]", request); + public ListenableFuture saveAttributesInternal(AttributesSaveRequest request) { TenantId tenantId = request.getTenantId(); EntityId entityId = request.getEntityId(); AttributesSaveRequest.Strategy strategy = request.getStrategy(); - ListenableFuture> resultFuture; + ListenableFuture resultFuture; if (strategy.saveAttributes()) { resultFuture = attrService.save(tenantId, entityId, request.getScope(), request.getEntries()); } else { - resultFuture = Futures.immediateFuture(Collections.emptyList()); + resultFuture = Futures.immediateFuture(AttributesSaveResult.EMPTY); } addMainCallback(resultFuture, result -> { @@ -229,6 +228,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer if (strategy.sendWsUpdate()) { addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries())); } + return resultFuture; } private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java index 8a76aa1d14..2d3be5a0ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; /** @@ -30,7 +31,7 @@ public interface InternalTelemetryService extends RuleEngineTelemetryService { ListenableFuture saveTimeseriesInternal(TimeseriesSaveRequest request); - void saveAttributesInternal(AttributesSaveRequest request); + ListenableFuture saveAttributesInternal(AttributesSaveRequest request); void deleteTimeseriesInternal(TimeseriesDeleteRequest request); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index cbe6663663..283e3baf76 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -36,6 +36,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.exception.RateLimitExceededException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -52,7 +53,6 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.TenantRateLimitException; import org.thingsboard.server.exception.UnauthorizedException; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -742,7 +742,7 @@ public class DefaultWebSocketService implements WebSocketService { @Override public void onFailure(Throwable e) { - if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) { + if (e instanceof RateLimitExceededException || e.getCause() instanceof RateLimitExceededException) { log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd); } else { log.info(FAILED_TO_FETCH_DATA, e); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e170695b90..c04969bfc7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -899,6 +899,8 @@ state: # Used only when state.persistToTelemetry is set to 'true' and Cassandra is used for timeseries data. # 0 means time-to-live mechanism is disabled. telemetryTtl: "${STATE_TELEMETRY_TTL:0}" + # Number of device records to fetch per batch when initializing device activity states + initFetchPackSize: "${TB_DEVICE_STATE_INIT_FETCH_PACK_SIZE:50000}" # Configuration properties for rule nodes related to device activity state rule: node: diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index f65f6bc629..8214a03616 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -464,7 +464,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes } @Test - public void testSimpleCalculatedFieldWhenPreserveMsgTsIsTrue() throws Exception { + public void testSimpleCalculatedFieldWhenUseLatestTsIsTrue() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); @@ -489,7 +489,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes output.setType(OutputType.TIME_SERIES); config.setOutput(output); - config.setPreserveMsgTs(true); + config.setUseLatestTs(true); calculatedField.setConfiguration(config); @@ -506,7 +506,69 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes } @Test - public void testScriptCalculatedFieldWhenUsedMsgTsInScript() throws Exception { + public void testSimpleCalculatedFieldWhenUseLatestTsIsTrueAndTelemetryBeforeLatest() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + long ts = System.currentTimeMillis(); + + long tsA = ts - 300000L; + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA))); + + long tsB = ts - 300L; + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB))); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("a + b"); + calculatedField.setDebugSettings(DebugSettings.all()); + calculatedField.setConfigurationVersion(1); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + argument1.setRefEntityKey(refEntityKey1); + Argument argument2 = new Argument(); + ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("b", ArgumentType.TS_LATEST, null); + argument2.setRefEntityKey(refEntityKey2); + config.setArguments(Map.of("a", argument1, "b", argument2)); + config.setExpression("a + b"); + + Output output = new Output(); + output.setName("c"); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + config.setUseLatestTs(true); + + calculatedField.setConfiguration(config); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode c = getLatestTelemetry(testDevice.getId(), "c"); + assertThat(c).isNotNull(); + assertThat(c.get("c").get(0).get("ts").asText()).isEqualTo(Long.toString(tsB)); + assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("6.0"); + }); + + long tsABeforeTsB = tsB - 300L; + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":10}}", tsABeforeTsB))); + + await().alias("update telemetry with ts less than latest -> save result with latest ts").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode c = getLatestTelemetry(testDevice.getId(), "c"); + assertThat(c).isNotNull(); + assertThat(c.get("c").get(0).get("ts").asText()).isEqualTo(Long.toString(tsB));// also tsB, since this is the latest timestamp + assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("11.0"); + }); + } + + @Test + public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); @@ -524,7 +586,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); config.setArguments(Map.of("T", argument)); - config.setExpression("return {\"ts\": ctx.msgTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); + config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); Output output = new Output(); output.setType(OutputType.TIME_SERIES); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java index 2c48ed9167..50cac8a6fe 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java @@ -17,8 +17,15 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -73,4 +80,34 @@ public class SingleValueArgumentEntryTest { void testUpdateEntryWhenValueWasNotChanged() { assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); } -} \ No newline at end of file + + @Test + void testToTbelCfArgWhenJsonIsObject() { + entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "{\"test\": 10}"), 370L); + TbelCfArg tbelCfArg = entry.toTbelCfArg(); + assertThat(tbelCfArg).isNotNull(); + assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class); + + TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg; + + assertThat(singleValueArg.getValue()).isInstanceOf(Map.class); + Map expectedMap = Map.of("test", 10); + assertThat(singleValueArg.getValue()).isEqualTo(expectedMap); + } + + @Test + void testToTbelCfArgWhenJsonIsArray() { + entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "[{\"test\": 10}, {\"test2\": 20}]"), 371L); + TbelCfArg tbelCfArg = entry.toTbelCfArg(); + assertThat(tbelCfArg).isNotNull(); + assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class); + + TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg; + + assertThat(singleValueArg.getValue()).isInstanceOf(List.class); + List> expectedList = new ArrayList<>(); + expectedList.add(Map.of("test", 10)); + expectedList.add(Map.of("test2", 20)); + assertThat(singleValueArg.getValue()).isEqualTo(expectedList); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index 264ac70443..d8bcd42bc8 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; @@ -395,7 +396,7 @@ public class EntityServiceTest extends AbstractControllerTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -545,7 +546,7 @@ public class EntityServiceTest extends AbstractControllerTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -599,7 +600,7 @@ public class EntityServiceTest extends AbstractControllerTest { List highConsumptions = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < assets.size(); i++) { Asset asset = assets.get(i); attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE)); @@ -1506,7 +1507,7 @@ public class EntityServiceTest extends AbstractControllerTest { } } - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); for (AttributeScope currentScope : AttributeScope.values()) { @@ -1578,7 +1579,7 @@ public class EntityServiceTest extends AbstractControllerTest { } } - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -1808,7 +1809,7 @@ public class EntityServiceTest extends AbstractControllerTest { } } - List>> attributeFutures = new ArrayList<>(); + List> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE)); @@ -2269,16 +2270,16 @@ public class EntityServiceTest extends AbstractControllerTest { return filter; } - private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { + private ListenableFuture saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { KvEntry attrValue = new LongDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); - return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); + return attributesService.save(tenantId, entityId, scope, List.of(attr)); } - private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { + private ListenableFuture saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { KvEntry attrValue = new StringDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); - return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); + return attributesService.save(tenantId, entityId, scope, List.of(attr)); } private ListenableFuture saveTimeseries(EntityId entityId, String key, Double value) { @@ -2294,8 +2295,8 @@ public class EntityServiceTest extends AbstractControllerTest { } protected void createMultiRootHierarchy(List buildings, List apartments, - Map> entityNameByTypeMap, - Map childParentRelationMap) throws InterruptedException { + Map> entityNameByTypeMap, + Map childParentRelationMap) throws InterruptedException { for (int k = 0; k < 3; k++) { Asset building = new Asset(); building.setTenantId(tenantId); diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index b23722afd5..8da1be43f1 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -89,7 +89,7 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testSubmitJob_allTasksSuccessful() { - int tasksCount = 5; + int tasksCount = 7; JobId jobId = submitJob(DummyJobConfiguration.builder() .successfulTasksCount(tasksCount) .taskProcessingTimeMs(1000) @@ -154,10 +154,10 @@ public class JobManagerTest extends AbstractControllerTest { @Test public void testCancelJob_whileRunning() throws Exception { - int tasksCount = 100; + int tasksCount = 200; JobId jobId = submitJob(DummyJobConfiguration.builder() .successfulTasksCount(tasksCount) - .taskProcessingTimeMs(100) + .taskProcessingTimeMs(50) .build()).getId(); Thread.sleep(500); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 282e959fcd..bab70ea505 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -825,6 +825,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRuleProcessor.process(ResourcesShortageTrigger.builder() .resource(Resource.RAM) .usage(15L) + .serviceType("serviceType") + .serviceId("serviceId") .build()); TimeUnit.MILLISECONDS.sleep(300); } @@ -837,10 +839,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRuleProcessor.process(ResourcesShortageTrigger.builder() .resource(Resource.RAM) .usage(5L) + .serviceType("serviceType") + .serviceId("serviceId") .build()); await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne()); } + @Test + public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception { + loginSysAdmin(); + ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder() + .ramThreshold(1f) + .cpuThreshold(1f) + .storageThreshold(1f) + .build(); + NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId()); + loginTenantAdmin(); + + Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo"); + method.setAccessible(true); + method.invoke(systemInfoService); + + TimeUnit.SECONDS.sleep(5); + assertThat(getMyNotifications(false, 100)).size().isZero(); + + loginSysAdmin(); + triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder() + .ramThreshold(0.01f) + .cpuThreshold(1f) + .storageThreshold(1f) + .build(); + rule.setTriggerConfig(triggerConfig); + saveNotificationRule(rule); + loginTenantAdmin(); + + method.invoke(systemInfoService); + + await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1); + Notification notification = getMyNotifications(false, 100).get(0); + assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage"); + assertThat(notification.getText()).isEqualTo("RAM shortage"); + } + @Test public void testNotificationRuleDisabling() throws Exception { EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index 26e913eacc..5bf87137ff 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -16,6 +16,9 @@ package org.thingsboard.server.service.state; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,13 +34,13 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; -import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; @@ -50,20 +53,22 @@ import org.thingsboard.server.dao.sql.query.EntityQueryRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.QueueKey; -import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.usagestats.DefaultTbApiUsageReportClient; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.time.Duration; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -77,8 +82,8 @@ import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -90,7 +95,10 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_DISCONNECT_TIME; @ExtendWith(MockitoExtension.class) -public class DefaultDeviceStateServiceTest { +class DefaultDeviceStateServiceTest { + + ListeningExecutorService deviceStateExecutor; + ListeningExecutorService deviceStateCallbackExecutor; @Mock DeviceService deviceService; @@ -113,25 +121,48 @@ public class DefaultDeviceStateServiceTest { @Mock DefaultTbApiUsageReportClient defaultTbApiUsageReportClient; - TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); - DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"); - TopicPartitionInfo tpi; + long defaultInactivityTimeoutMs = Duration.ofMinutes(10L).toMillis(); + + TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); + DeviceId deviceId = DeviceId.fromString("c209f718-42e5-11f0-9fe2-0242ac120002"); + TopicPartitionInfo tpi = TopicPartitionInfo.builder() + .topic("tb_core") + .partition(0) + .myPartition(true) + .build(); DefaultDeviceStateService service; @BeforeEach - public void setUp() { + void setUp() { service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, defaultTbApiUsageReportClient, notificationRuleProcessor)); ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService); + ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", defaultInactivityTimeoutMs); ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60); ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60); - ReflectionTestUtils.setField(service, "initFetchPackSize", 10); + ReflectionTestUtils.setField(service, "initFetchPackSize", 50000); + + deviceStateExecutor = MoreExecutors.newDirectExecutorService(); + ReflectionTestUtils.setField(service, "deviceStateExecutor", deviceStateExecutor); + + deviceStateCallbackExecutor = MoreExecutors.newDirectExecutorService(); + ReflectionTestUtils.setField(service, "deviceStateCallbackExecutor", deviceStateCallbackExecutor); + + lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi); + + ConcurrentMap> partitionedEntities = new ConcurrentHashMap<>(); + partitionedEntities.put(tpi, new HashSet<>()); + ReflectionTestUtils.setField(service, "partitionedEntities", partitionedEntities); + } - tpi = TopicPartitionInfo.builder().myPartition(true).build(); + @AfterEach + void cleanup() { + deviceStateExecutor.shutdownNow(); + deviceStateCallbackExecutor.shutdownNow(); } @Test - public void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() { + void givenDeviceBelongsToExternalPartition_whenOnDeviceConnect_thenCleansStateAndDoesNotReportConnect() { // GIVEN doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -149,7 +180,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) - public void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) { + void givenNegativeLastConnectTime_whenOnDeviceConnect_thenSkipsThisEvent(long negativeLastConnectTime) { // GIVEN doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -166,7 +197,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideOutdatedTimestamps") - public void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) { + void givenOutdatedLastConnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastConnectTime, long currentLastConnectTime) { // GIVEN doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -188,7 +219,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() { + void givenDeviceBelongsToMyPartition_whenOnDeviceConnect_thenReportsConnect() { // GIVEN var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) @@ -202,11 +233,13 @@ public class DefaultDeviceStateServiceTest { service.deviceStates.put(deviceId, deviceStateData); long lastConnectTime = System.currentTimeMillis(); + mockSuccessfulSaveAttributes(); + // WHEN service.onDeviceConnect(tenantId, deviceId, lastConnectTime); // THEN - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(LAST_CONNECT_TIME) && @@ -221,7 +254,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() { + void givenDeviceBelongsToExternalPartition_whenOnDeviceDisconnect_thenCleansStateAndDoesNotReportDisconnect() { // GIVEN doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -238,7 +271,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) - public void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) { + void givenNegativeLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long negativeLastDisconnectTime) { // GIVEN doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -254,7 +287,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideOutdatedTimestamps") - public void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) { + void givenOutdatedLastDisconnectTime_whenOnDeviceDisconnect_thenSkipsThisEvent(long outdatedLastDisconnectTime, long currentLastDisconnectTime) { // GIVEN doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -275,7 +308,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() { + void givenDeviceBelongsToMyPartition_whenOnDeviceDisconnect_thenReportsDisconnect() { // GIVEN var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) @@ -289,11 +322,13 @@ public class DefaultDeviceStateServiceTest { service.deviceStates.put(deviceId, deviceStateData); long lastDisconnectTime = System.currentTimeMillis(); + mockSuccessfulSaveAttributes(); + // WHEN service.onDeviceDisconnect(tenantId, deviceId, lastDisconnectTime); // THEN - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(LAST_DISCONNECT_TIME) && @@ -308,7 +343,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() { + void givenDeviceBelongsToExternalPartition_whenOnDeviceInactivity_thenCleansStateAndDoesNotReportInactivity() { // GIVEN doReturn(true).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -325,7 +360,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @ValueSource(longs = {Long.MIN_VALUE, -100, -1}) - public void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) { + void givenNegativeLastInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent(long negativeLastInactivityTime) { // GIVEN doReturn(false).when(service).cleanDeviceStateIfBelongsToExternalPartition(tenantId, deviceId); @@ -341,7 +376,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideOutdatedTimestamps") - public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( + void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentInactivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( long outdatedLastInactivityTime, long currentLastInactivityTime ) { // GIVEN @@ -365,7 +400,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideOutdatedTimestamps") - public void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( + void givenReceivedInactivityTimeIsLessThanOrEqualToCurrentActivityTime_whenOnDeviceInactivity_thenSkipsThisEvent( long outdatedLastInactivityTime, long currentLastActivityTime ) { // GIVEN @@ -398,7 +433,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { + void givenDeviceBelongsToMyPartition_whenOnDeviceInactivity_thenReportsInactivity() { // GIVEN var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) @@ -412,17 +447,19 @@ public class DefaultDeviceStateServiceTest { service.deviceStates.put(deviceId, deviceStateData); long lastInactivityTime = System.currentTimeMillis(); + mockSuccessfulSaveAttributes(); + // WHEN service.onDeviceInactivity(tenantId, deviceId, lastInactivityTime); // THEN - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getValue().equals(lastInactivityTime) )); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && @@ -445,7 +482,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() { + void givenInactivityTimeoutReached_whenUpdateInactivityStateIfExpired_thenReportsInactivity() { // GIVEN var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) @@ -456,16 +493,18 @@ public class DefaultDeviceStateServiceTest { given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); + mockSuccessfulSaveAttributes(); + // WHEN service.updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, deviceStateData); // THEN - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) )); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && @@ -488,7 +527,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { + void givenDeviceIdFromDeviceStatesMap_whenGetOrFetchDeviceStateData_thenNoStackOverflow() { service.deviceStates.put(deviceId, deviceStateDataMock); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); assertThat(deviceStateData).isEqualTo(deviceStateDataMock); @@ -496,7 +535,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { + void givenDeviceIdWithoutDeviceStateInMap_whenGetOrFetchDeviceStateData_thenFetchDeviceStateData() { service.deviceStates.clear(); willReturn(deviceStateDataMock).given(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); DeviceStateData deviceStateData = service.getOrFetchDeviceStateData(deviceId); @@ -504,172 +543,18 @@ public class DefaultDeviceStateServiceTest { verify(service).fetchDeviceStateDataUsingSeparateRequests(deviceId); } - private void initStateService(long timeout) throws InterruptedException { - service.stop(); - reset(service, telemetrySubscriptionService); - service.setDefaultInactivityTimeoutMs(timeout); - service.init(); - when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi); - when(entityQueryRepository.findEntityDataByQueryInternal(any())).thenReturn(new PageData<>()); - var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId()); - when(deviceService.findDeviceIdInfos(any())) - .thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false)); - PartitionChangeEvent event = new PartitionChangeEvent(this, ServiceType.TB_CORE, Map.of( - new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi) - ), Collections.emptyMap()); - service.onApplicationEvent(event); - Thread.sleep(100); - } - - @Test - public void increaseInactivityForInactiveDeviceTest() throws Exception { - final long defaultTimeout = 1; - initStateService(defaultTimeout); - DeviceState deviceState = DeviceState.builder().build(); - DeviceStateData deviceStateData = DeviceStateData.builder() - .tenantId(tenantId) - .deviceId(deviceId) - .state(deviceState) - .metaData(new TbMsgMetaData()) - .build(); - - service.deviceStates.put(deviceId, deviceStateData); - service.getPartitionedEntities(tpi).add(deviceId); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - Thread.sleep(defaultTimeout); - service.checkStates(); - activityVerify(false); - - reset(telemetrySubscriptionService); - - long increase = 100; - long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; - - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); - activityVerify(true); - Thread.sleep(increase); - service.checkStates(); - activityVerify(false); - - reset(telemetrySubscriptionService); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - Thread.sleep(newTimeout + 5); - service.checkStates(); - activityVerify(false); - } - - @Test - public void increaseInactivityForActiveDeviceTest() throws Exception { - final long defaultTimeout = 1000; - initStateService(defaultTimeout); - DeviceState deviceState = DeviceState.builder().build(); - DeviceStateData deviceStateData = DeviceStateData.builder() - .tenantId(tenantId) - .deviceId(deviceId) - .state(deviceState) - .metaData(new TbMsgMetaData()) - .build(); - - service.deviceStates.put(deviceId, deviceStateData); - service.getPartitionedEntities(tpi).add(deviceId); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - - reset(telemetrySubscriptionService); - - long increase = 100; - long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase; - - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); - verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request -> - request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) - )); - Thread.sleep(defaultTimeout + increase); - service.checkStates(); - activityVerify(false); - - reset(telemetrySubscriptionService); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - Thread.sleep(newTimeout); - service.checkStates(); - activityVerify(false); - } - - @Test - public void increaseSmallInactivityForInactiveDeviceTest() throws Exception { - final long defaultTimeout = 1; - initStateService(defaultTimeout); - DeviceState deviceState = DeviceState.builder().build(); - DeviceStateData deviceStateData = DeviceStateData.builder() - .tenantId(tenantId) - .deviceId(deviceId) - .state(deviceState) - .metaData(new TbMsgMetaData()) - .build(); - - service.deviceStates.put(deviceId, deviceStateData); - service.getPartitionedEntities(tpi).add(deviceId); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - Thread.sleep(defaultTimeout); - service.checkStates(); - activityVerify(false); - - reset(telemetrySubscriptionService); - - long newTimeout = 1; - Thread.sleep(newTimeout); - verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request -> - request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) - )); - } + @MethodSource + @ParameterizedTest + void testOnDeviceInactivityTimeoutUpdate(boolean initialActivityStatus, long newInactivityTimeout, boolean expectedActivityStatus) { + // GIVEN + doReturn(200L).when(service).getCurrentTimeMillis(); - @Test - public void decreaseInactivityForActiveDeviceTest() throws Exception { - final long defaultTimeout = 1000; - initStateService(defaultTimeout); - DeviceState deviceState = DeviceState.builder().build(); - DeviceStateData deviceStateData = DeviceStateData.builder() - .tenantId(tenantId) - .deviceId(deviceId) - .state(deviceState) - .metaData(new TbMsgMetaData()) + var deviceState = DeviceState.builder() + .active(initialActivityStatus) + .lastActivityTime(100L) .build(); - service.deviceStates.put(deviceId, deviceStateData); - service.getPartitionedEntities(tpi).add(deviceId); - - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - - long newTimeout = 1; - Thread.sleep(newTimeout); - - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); - activityVerify(false); - reset(telemetrySubscriptionService); - - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout); - activityVerify(true); - Thread.sleep(defaultTimeout); - service.checkStates(); - activityVerify(false); - } - - @Test - public void decreaseInactivityForInactiveDeviceTest() throws Exception { - final long defaultTimeout = 1000; - initStateService(defaultTimeout); - DeviceState deviceState = DeviceState.builder().build(); - DeviceStateData deviceStateData = DeviceStateData.builder() + var deviceStateData = DeviceStateData.builder() .tenantId(tenantId) .deviceId(deviceId) .state(deviceState) @@ -679,31 +564,44 @@ public class DefaultDeviceStateServiceTest { service.deviceStates.put(deviceId, deviceStateData); service.getPartitionedEntities(tpi).add(deviceId); - service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); - activityVerify(true); - Thread.sleep(defaultTimeout); - service.checkStates(); - activityVerify(false); - reset(telemetrySubscriptionService); + mockSuccessfulSaveAttributes(); - long newTimeout = 1; + // WHEN + service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newInactivityTimeout); - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout); - verify(telemetrySubscriptionService, never()).saveAttributes(argThat(request -> - request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) - )); + // THEN + long expectedInactivityTimeout = newInactivityTimeout != 0 ? newInactivityTimeout : defaultInactivityTimeoutMs; + assertThat(deviceState.getInactivityTimeout()).isEqualTo(expectedInactivityTimeout); + + assertThat(deviceState.isActive()).isEqualTo(expectedActivityStatus); + if (initialActivityStatus != expectedActivityStatus) { + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> { + AttributeKvEntry entry = request.getEntries().get(0); + return request.getEntityId().equals(deviceId) && entry.getKey().equals(ACTIVITY_STATE) && entry.getValue().equals(expectedActivityStatus); + })); + } } - private void activityVerify(boolean isActive) { - verify(telemetrySubscriptionService).saveAttributes(argThat(request -> - request.getEntityId().equals(deviceId) && - request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && - request.getEntries().get(0).getValue().equals(isActive) - )); + // to simplify test, these arguments assume that the current time is 200 and the last activity time is 100 + private static Stream testOnDeviceInactivityTimeoutUpdate() { + return Stream.of( + Arguments.of(true, 1L, false), + Arguments.of(true, 50L, false), + Arguments.of(true, 99L, false), + Arguments.of(true, 100L, false), + Arguments.of(true, 101L, true), + Arguments.of(true, 0L, true), // should use default inactivity timeout of 10 minutes + Arguments.of(false, 1L, false), + Arguments.of(false, 50L, false), + Arguments.of(false, 99L, false), + Arguments.of(false, 100L, false), + Arguments.of(false, 101L, true), + Arguments.of(false, 0L, true) // should use default inactivity timeout of 10 minutes + ); } @Test - public void givenStateDataIsNull_whenUpdateActivityState_thenShouldCleanupDevice() { + void givenStateDataIsNull_whenUpdateActivityState_thenShouldCleanupDevice() { // GIVEN service.deviceStates.put(deviceId, deviceStateDataMock); @@ -719,7 +617,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideParametersForUpdateActivityState") - public void givenTestParameters_whenUpdateActivityState_thenShouldBeInTheExpectedStateAndPerformExpectedActions( + void givenTestParameters_whenUpdateActivityState_thenShouldBeInTheExpectedStateAndPerformExpectedActions( boolean activityState, long previousActivityTime, long lastReportedActivity, long inactivityAlarmTime, long expectedInactivityAlarmTime, boolean shouldSetInactivityAlarmTimeToZero, boolean shouldUpdateActivityStateToActive @@ -739,13 +637,15 @@ public class DefaultDeviceStateServiceTest { .metaData(new TbMsgMetaData()) .build(); + mockSuccessfulSaveAttributes(); + // WHEN service.updateActivityState(deviceId, deviceStateData, lastReportedActivity); // THEN assertThat(deviceState.isActive()).isEqualTo(true); assertThat(deviceState.getLastActivityTime()).isEqualTo(lastReportedActivity); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(LAST_ACTIVITY_TIME) && request.getEntries().get(0).getValue().equals(lastReportedActivity) @@ -753,7 +653,7 @@ public class DefaultDeviceStateServiceTest { assertThat(deviceState.getLastInactivityAlarmTime()).isEqualTo(expectedInactivityAlarmTime); if (shouldSetInactivityAlarmTimeToZero) { - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && request.getEntries().get(0).getValue().equals(0L) @@ -761,7 +661,7 @@ public class DefaultDeviceStateServiceTest { } if (shouldUpdateActivityStateToActive) { - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(true) @@ -809,59 +709,8 @@ public class DefaultDeviceStateServiceTest { ); } - @ParameterizedTest - @MethodSource("provideParametersForDecreaseInactivityTimeout") - public void givenTestParameters_whenOnDeviceInactivityTimeout_thenShouldBeInTheExpectedStateAndPerformExpectedActions( - boolean activityState, long newInactivityTimeout, long timeIncrement, boolean expectedActivityState - ) throws Exception { - // GIVEN - long defaultInactivityTimeout = 10000; - initStateService(defaultInactivityTimeout); - - var currentTime = new AtomicLong(System.currentTimeMillis()); - - DeviceState deviceState = DeviceState.builder() - .active(activityState) - .lastActivityTime(currentTime.get()) - .inactivityTimeout(defaultInactivityTimeout) - .build(); - - DeviceStateData deviceStateData = DeviceStateData.builder() - .tenantId(tenantId) - .deviceId(deviceId) - .state(deviceState) - .metaData(new TbMsgMetaData()) - .build(); - - service.deviceStates.put(deviceId, deviceStateData); - service.getPartitionedEntities(tpi).add(deviceId); - - given(service.getCurrentTimeMillis()).willReturn(currentTime.addAndGet(timeIncrement)); - - // WHEN - service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newInactivityTimeout); - - // THEN - assertThat(deviceState.getInactivityTimeout()).isEqualTo(newInactivityTimeout); - assertThat(deviceState.isActive()).isEqualTo(expectedActivityState); - if (activityState && !expectedActivityState) { - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> - request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && - request.getEntries().get(0).getValue().equals(false) - )); - } - } - - private static Stream provideParametersForDecreaseInactivityTimeout() { - return Stream.of( - Arguments.of(true, 1, 0, true), - - Arguments.of(true, 1, 1, false) - ); - } - @Test - public void givenStateDataIsNull_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { + void givenStateDataIsNull_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { // GIVEN service.deviceStates.put(deviceId, deviceStateDataMock); @@ -875,7 +724,7 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenNotMyPartition_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { + void givenNotMyPartition_whenUpdateInactivityTimeoutIfExpired_thenShouldCleanupDevice() { // GIVEN long currentTime = System.currentTimeMillis(); @@ -911,7 +760,7 @@ public class DefaultDeviceStateServiceTest { @ParameterizedTest @MethodSource("provideParametersForUpdateInactivityStateIfExpired") - public void givenTestParameters_whenUpdateInactivityStateIfExpired_thenShouldBeInTheExpectedStateAndPerformExpectedActions( + void givenTestParameters_whenUpdateInactivityStateIfExpired_thenShouldBeInTheExpectedStateAndPerformExpectedActions( boolean activityState, long ts, long lastActivityTime, long lastInactivityAlarmTime, long inactivityTimeout, long deviceCreationTime, boolean expectedActivityState, long expectedLastInactivityAlarmTime, boolean shouldUpdateActivityStateToInactive ) { @@ -933,6 +782,7 @@ public class DefaultDeviceStateServiceTest { if (shouldUpdateActivityStateToInactive) { given(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).willReturn(tpi); + mockSuccessfulSaveAttributes(); } // WHEN @@ -943,7 +793,7 @@ public class DefaultDeviceStateServiceTest { assertThat(state.getLastInactivityAlarmTime()).isEqualTo(expectedLastInactivityAlarmTime); if (shouldUpdateActivityStateToInactive) { - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(false) )); @@ -961,7 +811,7 @@ public class DefaultDeviceStateServiceTest { assertThat(actualNotification.getDeviceId()).isEqualTo(deviceId); assertThat(actualNotification.isActive()).isFalse(); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getTenantId().equals(tenantId) && request.getEntityId().equals(deviceId) && request.getScope().equals(AttributeScope.SERVER_SCOPE) && request.getEntries().get(0).getKey().equals(INACTIVITY_ALARM_TIME) && @@ -1033,7 +883,79 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { + void givenInactiveDevice_whenActivityStatusChangesToActiveButFailedToSaveUpdatedActivityStatus_thenShouldNotUpdateCache2() { + // GIVEN + var deviceState = DeviceState.builder() + .active(false) + .lastActivityTime(100L) + .inactivityTimeout(50L) + .build(); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + // WHEN-THEN + + // simulating short DB outage + given(telemetrySubscriptionService.saveAttributesInternal(any())).willReturn(Futures.immediateFailedFuture(new RuntimeException("failed to save"))); + doReturn(200L).when(service).getCurrentTimeMillis(); + service.onDeviceActivity(tenantId, deviceId, 180L); + assertThat(deviceState.isActive()).isFalse(); // still inactive + + // 10 millis pass... and new activity message it received + + // this time DB save is successful + when(telemetrySubscriptionService.saveAttributesInternal(any())).thenReturn(Futures.immediateFuture(AttributesSaveResult.of(generateRandomVersions(1)))); + doReturn(210L).when(service).getCurrentTimeMillis(); + service.onDeviceActivity(tenantId, deviceId, 190L); + assertThat(deviceState.isActive()).isTrue(); + } + + @Test + void givenActiveDevice_whenActivityStatusChangesToInactiveButFailedToSaveUpdatedActivityStatus_thenShouldNotUpdateCache() { + // GIVEN + var deviceState = DeviceState.builder() + .active(true) + .lastActivityTime(100L) + .inactivityTimeout(50L) + .build(); + + var deviceStateData = DeviceStateData.builder() + .tenantId(tenantId) + .deviceId(deviceId) + .state(deviceState) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + service.deviceStates.put(deviceId, deviceStateData); + service.getPartitionedEntities(tpi).add(deviceId); + + // WHEN-THEN (assuming periodic activity states check is done every 100 millis) + + // simulating short DB outage + given(telemetrySubscriptionService.saveAttributesInternal(any())).willReturn(Futures.immediateFailedFuture(new RuntimeException("failed to save"))); + doReturn(200L).when(service).getCurrentTimeMillis(); + service.checkStates(); + assertThat(deviceState.isActive()).isTrue(); // still active + + // waiting 100 millis... periodic activity states check is triggered again + + // this time DB save is successful + when(telemetrySubscriptionService.saveAttributesInternal(any())).thenReturn(Futures.immediateFuture(AttributesSaveResult.of(generateRandomVersions(1)))); + doReturn(300L).when(service).getCurrentTimeMillis(); + service.checkStates(); + assertThat(deviceState.isActive()).isFalse(); + } + + @Test + void givenConcurrentAccess_whenGetOrFetchDeviceStateData_thenFetchDeviceStateDataInvokedOnce() { doAnswer(invocation -> { Thread.sleep(100); return deviceStateDataMock; @@ -1069,10 +991,8 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException { + void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() { // GIVEN - final long defaultTimeout = 1000; - initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); @@ -1086,13 +1006,15 @@ public class DefaultDeviceStateServiceTest { .setDeleted(false) .build(); + mockSuccessfulSaveAttributes(); + // WHEN service.onQueueMsg(proto, TbCallback.EMPTY); // THEN await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(false) )); @@ -1100,14 +1022,12 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException { + void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() { // GIVEN - final long defaultTimeout = 1000; - initStateService(defaultTimeout); long currentTime = System.currentTimeMillis(); DeviceState deviceState = DeviceState.builder() .active(false) - .inactivityTimeout(service.getDefaultInactivityTimeoutInSec()) + .inactivityTimeout(defaultInactivityTimeoutMs) .build(); DeviceStateData stateData = DeviceStateData.builder() .tenantId(tenantId) @@ -1118,12 +1038,14 @@ public class DefaultDeviceStateServiceTest { .build(); service.deviceStates.put(deviceId, stateData); + mockSuccessfulSaveAttributes(); + // WHEN service.onDeviceActivity(tenantId, deviceId, currentTime); // THEN ArgumentCaptor attributeRequestCaptor = ArgumentCaptor.forClass(AttributesSaveRequest.class); - then(telemetrySubscriptionService).should(times(2)).saveAttributes(attributeRequestCaptor.capture()); + then(telemetrySubscriptionService).should(times(2)).saveAttributesInternal(attributeRequestCaptor.capture()); await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); @@ -1151,15 +1073,14 @@ public class DefaultDeviceStateServiceTest { } @Test - public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException { + void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() { // GIVEN - final long defaultTimeout = 1000; - initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); long currentTime = System.currentTimeMillis(); - DeviceState deviceState = DeviceState.builder() + + var deviceState = DeviceState.builder() .active(true) .lastConnectTime(currentTime - 8000) .lastActivityTime(currentTime - 4000) @@ -1167,16 +1088,20 @@ public class DefaultDeviceStateServiceTest { .lastInactivityAlarmTime(0) .inactivityTimeout(3000) .build(); - DeviceStateData stateData = DeviceStateData.builder() + + var stateData = DeviceStateData.builder() .tenantId(tenantId) .deviceId(deviceId) .deviceCreationTime(currentTime - 10000) .state(deviceState) .build(); + service.deviceStates.put(deviceId, stateData); + mockSuccessfulSaveAttributes(); + // WHEN - TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() + var proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) @@ -1190,11 +1115,25 @@ public class DefaultDeviceStateServiceTest { // THEN await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true); - then(telemetrySubscriptionService).should().saveAttributes(argThat(request -> + then(telemetrySubscriptionService).should().saveAttributesInternal(argThat(request -> request.getEntityId().equals(deviceId) && request.getEntries().get(0).getKey().equals(ACTIVITY_STATE) && request.getEntries().get(0).getValue().equals(true) )); }); } + private void mockSuccessfulSaveAttributes() { + lenient().when(telemetrySubscriptionService.saveAttributesInternal(any())).thenAnswer(invocation -> { + AttributesSaveRequest request = invocation.getArgument(0); + return Futures.immediateFuture(generateRandomVersions(request.getEntries().size())); + }); + } + + private static List generateRandomVersions(int n) { + return ThreadLocalRandom.current() + .longs(n) + .boxed() + .toList(); + } + } 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 index 2b4d9f38e5..153228a865 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; @@ -472,7 +473,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(saveAttributes, sendWsUpdate, processCalculatedFields)) .build(); - lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries())).thenReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries())) + .thenReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -547,7 +549,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), entries)) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size())))); // WHEN telemetryService.saveAttributes(request); @@ -581,7 +584,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size())))); // WHEN telemetryService.saveAttributes(request); @@ -613,7 +617,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), entries)) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size())))); // WHEN telemetryService.saveAttributes(request); @@ -640,7 +645,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), entries)) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(entries.size())))); // WHEN telemetryService.saveAttributes(request); @@ -715,7 +721,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -764,7 +771,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -792,7 +800,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -815,7 +824,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -843,7 +853,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); @@ -870,7 +881,8 @@ class DefaultTelemetrySubscriptionServiceTest { .strategy(new AttributesSaveRequest.Strategy(true, false, false)) .build(); - given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())) + .willReturn(immediateFuture(AttributesSaveResult.of(listOfNNumbers(request.getEntries().size())))); // WHEN telemetryService.saveAttributes(request); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 718c574c3c..0d5d3dcd13 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import java.util.Collection; import java.util.List; @@ -37,9 +38,9 @@ public interface AttributesService { ListenableFuture> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope); - ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes); + ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes); - ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); + ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributeKeys); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java index c979fbffd1..afbad6e3a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java @@ -20,6 +20,7 @@ import lombok.Data; @Data public class SystemInfoData { + @Schema(description = "Service Id.") private String serviceId; @Schema(description = "Service type.") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index af3cb4d5cd..86c7b9e9b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; @EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { - private boolean preserveMsgTs; + private boolean useLatestTs; @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java index 3df7fc92fe..1e890da961 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java @@ -20,7 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.apache.commons.lang3.BooleanUtils; + +import static org.apache.commons.lang3.BooleanUtils.toBooleanDefaultIfNull; @Getter @NoArgsConstructor @@ -34,14 +35,14 @@ public class EdqsState { private EdqsApiMode apiMode; public boolean updateEdqsReady(boolean ready) { - boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready; + boolean changed = toBooleanDefaultIfNull(this.edqsReady, false) != ready; this.edqsReady = ready; return changed; } @JsonIgnore public boolean isApiReady() { - return edqsReady && syncStatus == EdqsSyncStatus.FINISHED; + return toBooleanDefaultIfNull(edqsReady, false) && syncStatus == EdqsSyncStatus.FINISHED; } @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java index 1988f13eb0..5b913af3e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job.task; +import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -25,22 +26,25 @@ import org.thingsboard.server.common.data.job.JobType; @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor -@SuperBuilder @ToString(callSuper = true) public class DummyTaskResult extends TaskResult { private DummyTaskFailure failure; + @Builder + private DummyTaskResult(boolean success, boolean discarded, DummyTaskFailure failure) { + super(success, discarded); + this.failure = failure; + } + public static DummyTaskResult success(DummyTask task) { return DummyTaskResult.builder() - .key(task.getKey()) .success(true) .build(); } public static DummyTaskResult failed(DummyTask task, Throwable error) { return DummyTaskResult.builder() - .key(task.getKey()) .failure(DummyTaskFailure.builder() .error(error.getMessage()) .number(task.getNumber()) @@ -51,7 +55,6 @@ public class DummyTaskResult extends TaskResult { public static DummyTaskResult discarded(DummyTask task) { return DummyTaskResult.builder() - .key(task.getKey()) .discarded(true) .build(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 21303a55fe..da3c8252eb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -20,16 +20,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; import org.thingsboard.server.common.data.job.JobType; @Data -@AllArgsConstructor @NoArgsConstructor -@SuperBuilder @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ @@ -40,6 +36,12 @@ public abstract class TaskResult { private String key; private boolean success; private boolean discarded; + private long finishTs; + + protected TaskResult(boolean success, boolean discarded) { + this.success = success; + this.discarded = discarded; + } @JsonIgnore public abstract JobType getJobType(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributesSaveResult.java similarity index 57% rename from dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java rename to common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributesSaveResult.java index 3d79af980d..711a3e06a4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributesSaveResult.java @@ -13,7 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.util; +package org.thingsboard.server.common.data.kv; + +import java.util.Collections; +import java.util.List; + +public record AttributesSaveResult(List versions) { + + public static final AttributesSaveResult EMPTY = new AttributesSaveResult(Collections.emptyList()); + + public static AttributesSaveResult of(List versions) { + if (versions == null) { + return EMPTY; + } + return new AttributesSaveResult(versions); + } -public class TenantRateLimitException extends Exception { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index ef839247ab..3dc063ccca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -43,6 +43,7 @@ public enum LimitedApi { RateLimitUtil.merge( DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits, DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true), + CASSANDRA_QUERIES(null, true), // left for backward compatibility with RateLimitsNotificationInfo EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true), EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false), EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java index 24cb21febd..c05a10ef8f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java @@ -30,12 +30,16 @@ public class ResourcesShortageNotificationInfo implements RuleOriginatedNotifica private String resource; private Long usage; + private String serviceId; + private String serviceType; @Override public Map getTemplateData() { return Map.of( "resource", resource, - "usage", String.valueOf(usage) + "usage", String.valueOf(usage), + "serviceId", serviceId, + "serviceType", serviceType ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java index 4124eb04f8..5672b2c98c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java @@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; import java.util.concurrent.TimeUnit; @Data @Builder public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = 2918443863787603524L; + private final TenantId tenantId; private final CustomerId customerId; private final EdgeId edgeId; @@ -37,8 +41,8 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger private final String error; @Override - public boolean deduplicate() { - return true; + public DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.ALL; } @Override @@ -60,4 +64,5 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger public EntityId getOriginatorEntityId() { return edgeId; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java index fc3b69e697..0da465ec09 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java @@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; import java.util.concurrent.TimeUnit; @Data @Builder public class EdgeConnectionTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -261939829962721957L; + private final TenantId tenantId; private final CustomerId customerId; private final EdgeId edgeId; @@ -36,8 +40,8 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger { private final String edgeName; @Override - public boolean deduplicate() { - return true; + public DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.ALL; } @Override @@ -59,4 +63,5 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger { public EntityId getOriginatorEntityId() { return edgeId; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java index 204ae15e57..50ee0768d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java @@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class NewPlatformVersionTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = 3298785969736390092L; + private final UpdateMessage updateInfo; @Override @@ -45,8 +50,8 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger { @Override - public boolean deduplicate() { - return true; + public DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.ALL; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java index 31940d6ac8..f6cf398b44 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java @@ -29,9 +29,8 @@ public interface NotificationRuleTrigger extends Serializable { EntityId getOriginatorEntityId(); - - default boolean deduplicate() { - return false; + default DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.NONE; } default String getDeduplicationKey() { @@ -43,4 +42,10 @@ public interface NotificationRuleTrigger extends Serializable { return 0; } + enum DeduplicationStrategy { + NONE, + ALL, + ONLY_MATCHING + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java index 39d570a9e0..37e984c6f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java @@ -22,12 +22,16 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; import java.util.concurrent.TimeUnit; @Data @Builder public class RateLimitsTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -4423112145409424886L; + private final TenantId tenantId; private final LimitedApi api; private final EntityId limitLevel; @@ -45,8 +49,8 @@ public class RateLimitsTrigger implements NotificationRuleTrigger { @Override - public boolean deduplicate() { - return true; + public DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.ALL; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java index f12c80d5db..be2485c959 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java @@ -33,6 +33,8 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger { private Resource resource; private Long usage; + private String serviceId; + private String serviceType; @Override public TenantId getTenantId() { @@ -45,13 +47,13 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger { } @Override - public boolean deduplicate() { - return true; + public DeduplicationStrategy getDeduplicationStrategy() { + return DeduplicationStrategy.ONLY_MATCHING; } @Override public String getDeduplicationKey() { - return resource.name(); + return String.join(":", resource.name(), serviceId, serviceType); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 510d2c3a41..0e74cb98fa 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -277,6 +277,7 @@ public class EdqsProcessor implements TbQueueHandler, eventConsumer.awaitStop(); responseTemplate.stop(); stateService.stop(); + versionsStore.shutdown(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 66bbb7a68a..7e2e99e662 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -224,6 +224,7 @@ public class KafkaEdqsStateService implements EdqsStateService { stateConsumer.awaitStop(); eventsToBackupConsumer.stop(); stateProducer.stop(); + versionsStore.shutdown(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java index ba3263eec2..f348e9cf9e 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java @@ -15,31 +15,35 @@ */ package org.thingsboard.server.edqs.util; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.edqs.EdqsObjectKey; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @Slf4j public class VersionsStore { - private final Cache versions; + private final ConcurrentMap versions = new ConcurrentHashMap<>(); + private final long expirationMillis; + private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(); public VersionsStore(int ttlMinutes) { - this.versions = Caffeine.newBuilder() - .expireAfterWrite(ttlMinutes, TimeUnit.MINUTES) - .build(); + this.expirationMillis = TimeUnit.MINUTES.toMillis(ttlMinutes); + startCleanupTask(); } public boolean isNew(EdqsObjectKey key, Long version) { AtomicBoolean isNew = new AtomicBoolean(false); - versions.asMap().compute(key, (k, prevVersion) -> { - if (prevVersion == null || prevVersion <= version) { + versions.compute(key, (k, prevVersion) -> { + if (prevVersion == null || prevVersion.value <= version) { isNew.set(true); - return version; + return new TimedValue(version); } else { log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); return prevVersion; @@ -48,4 +52,33 @@ public class VersionsStore { return isNew.get(); } + private void startCleanupTask() { + cleaner.scheduleAtFixedRate(() -> { + try { + long now = System.currentTimeMillis(); + for (Map.Entry entry : versions.entrySet()) { + if (now - entry.getValue().lastUpdated > expirationMillis) { + versions.remove(entry.getKey(), entry.getValue()); + } + } + } catch (Exception e) { + log.error("Cleanup task failed", e); + } + }, expirationMillis, expirationMillis, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + cleaner.shutdown(); + } + + private static class TimedValue { + private final long lastUpdated; + private final long value; + + public TimedValue(long value) { + this.value = value; + this.lastUpdated = System.currentTimeMillis(); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java index 194f0ce962..a410ac5d04 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -47,7 +48,7 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso @Override public void process(NotificationRuleTrigger trigger) { try { - if (trigger.deduplicate() && deduplicationService.alreadyProcessed(trigger)) { + if (!DeduplicationStrategy.NONE.equals(trigger.getDeduplicationStrategy()) && deduplicationService.alreadyProcessed(trigger)) { return; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java index 62ca19a05f..33c52859ca 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java @@ -232,6 +232,8 @@ public abstract class TaskProcessor, R extends TaskResult> { } private void reportTaskResult(T task, R result) { + result.setKey(task.getKey()); + result.setFinishTs(System.currentTimeMillis()); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java index 7515cb5269..c6023154ea 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java @@ -25,11 +25,11 @@ public class TbelCfCtx implements TbelCfObject { @Getter private final Map args; @Getter - private final long msgTs; + private final long latestTs; - public TbelCfCtx(Map args, long lastUpdateTs) { + public TbelCfCtx(Map args, long latestTs) { this.args = Collections.unmodifiableMap(args); - this.msgTs = lastUpdateTs != -1 ? lastUpdateTs : System.currentTimeMillis(); + this.latestTs = latestTs != -1 ? latestTs : System.currentTimeMillis(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 777a77d054..9803670d4b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.Validator; @@ -41,7 +42,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @@ -101,26 +101,29 @@ public class BaseAttributesService implements AttributesService { } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); - return doSave(tenantId, entityId, scope, attribute); + return doSave(tenantId, entityId, scope, List.of(attribute)); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); - List> saveFutures = attributes.stream().map(attribute -> doSave(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); - return Futures.allAsList(saveFutures); + return doSave(tenantId, entityId, scope, attributes); } - private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, version -> { - edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); - return version; - }, MoreExecutors.directExecutor()); + private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { + List> futures = new ArrayList<>(attributes.size()); + for (AttributeKvEntry attribute : attributes) { + ListenableFuture future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> { + edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); + return version; + }, MoreExecutors.directExecutor()); + futures.add(future); + } + return Futures.transform(Futures.allAsList(futures), AttributesSaveResult::of, MoreExecutors.directExecutor()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 559828911f..d99413f13a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.edqs.EdqsService; @@ -56,7 +57,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @@ -150,7 +150,7 @@ public class CachedAttributesService implements AttributesService { List cachedAttributes = wrappedCachedAttributes.values().stream() .map(TbCacheValueWrapper::get) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); if (wrappedCachedAttributes.size() == attributeKeys.size()) { log.trace("[{}][{}] Found all attributes from cache: {}", entityId, scope, attributeKeys); return Futures.immediateFuture(cachedAttributes); @@ -159,8 +159,6 @@ public class CachedAttributesService implements AttributesService { Set notFoundAttributeKeys = new HashSet<>(attributeKeys); notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); - List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); - // DB call should run in DB executor, not in cache-related executor return jpaExecutorService.submit(() -> { log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); @@ -222,33 +220,31 @@ public class CachedAttributesService implements AttributesService { } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); - return doSave(tenantId, entityId, scope, attribute); + return doSave(tenantId, entityId, scope, List.of(attribute)); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); + return doSave(tenantId, entityId, scope, attributes); + } + private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { - futures.add(doSave(tenantId, entityId, scope, attribute)); + ListenableFuture future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> { + BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version); + put(entityId, scope, attributeKvEntry); + edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version)); + return version; + }, cacheExecutor); + futures.add(future); } - - return Futures.allAsList(futures); - } - - private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, version -> { - BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version); - put(entityId, scope, attributeKvEntry); - edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version)); - return version; - }, cacheExecutor); + return Futures.transform(Futures.allAsList(futures), AttributesSaveResult::of, MoreExecutors.directExecutor()); } private void put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { @@ -270,7 +266,7 @@ public class CachedAttributesService implements AttributesService { edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); } return key; - }, cacheExecutor)).collect(Collectors.toList())); + }, cacheExecutor)).toList()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 153e95a404..360aa0063b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -69,7 +69,6 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(QUEUED); } else { job.setStatus(PENDING); - job.getResult().setStartTs(System.currentTimeMillis()); } return saveJob(tenantId, job, true, null); } @@ -125,6 +124,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } boolean publishEvent = false; + long lastFinishTs = 0; for (TaskResult taskResult : jobStats.getTaskResults()) { if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) { log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey()); @@ -140,6 +140,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi publishEvent = true; } } + if (taskResult.getFinishTs() > lastFinishTs) { + lastFinishTs = taskResult.getFinishTs(); + } } if (job.getStatus() == RUNNING) { @@ -153,7 +156,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(COMPLETED); publishEvent = true; } - result.setFinishTs(System.currentTimeMillis()); + result.setFinishTs(lastFinishTs); job.getConfiguration().setToReprocess(null); } } @@ -166,6 +169,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) { throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType()); } + if (job.getStatus() == PENDING) { + job.getResult().setStartTs(System.currentTimeMillis()); + } job = jobDao.save(tenantId, job); if (publishEvent) { 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 9b8b8b255e..7cee3d04ae 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 @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig; @@ -377,7 +376,7 @@ public class DefaultNotifications { public static final DefaultNotification resourcesShortage = DefaultNotification.builder() .name("Resources shortage notification") .type(NotificationType.RESOURCES_SHORTAGE) - .subject("Warning: ${resource} shortage") + .subject("Warning: ${resource} shortage for ${serviceId}") .text("${resource} usage is at ${usage}%.") .icon("warning") .rule(DefaultRule.builder() diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 9eefcaae1e..cecf4ab587 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -205,8 +205,8 @@ public class BaseTimeseriesService implements TimeseriesService { ListenableFuture dpsFuture = saveTs ? Futures.transform(Futures.allAsList(tsFutures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()) : Futures.immediateFuture(0); ListenableFuture> versionsFuture = saveLatest ? Futures.allAsList(latestFutures) : Futures.immediateFuture(null); return Futures.whenAllComplete(dpsFuture, versionsFuture).call(() -> { - Integer dataPoints = Futures.getUnchecked(dpsFuture); - List versions = Futures.getUnchecked(versionsFuture); + Integer dataPoints = dpsFuture.get(); + List versions = versionsFuture.get(); return TimeseriesSaveResult.of(dataPoints, versions); }, MoreExecutors.directExecutor()); } @@ -298,13 +298,13 @@ public class BaseTimeseriesService implements TimeseriesService { long interval = query.getInterval(); if (interval < 1) { throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval + - ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); + ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); } long step = Math.max(interval, 1000); long intervalCounts = (query.getEndTs() - query.getStartTs()) / step; if (intervalCounts > maxTsIntervals || intervalCounts < 0) { throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " + - "Please increase 'interval' parameter for your query or reduce the time range of the query."); + "Please increase 'interval' parameter for your query or reduce the time range of the query."); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index cbcf3e81ec..4d691db31d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cache.limits.RateLimitService; +import org.thingsboard.server.common.data.exception.RateLimitExceededException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -66,7 +67,7 @@ public abstract class AbstractBufferedRateExecutor> queue; private final ExecutorService dispatcherExecutor; private final ExecutorService callbackExecutor; @@ -124,7 +125,7 @@ public abstract class AbstractBufferedRateExecutor 0 - || rateLimitedTenantsCount > 0 - || concurrencyLevel.get() > 0 - || stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0) + || rateLimitedTenantsCount > 0 + || concurrencyLevel.get() > 0 + || stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0) ) { StringBuilder statsBuilder = new StringBuilder(); diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 15708fbdbf..d72efb3aa6 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -41,7 +41,6 @@ ${project.build.directory}/windows ThingsBoard Monitoring Service org.thingsboard.monitoring.ThingsboardMonitoringApplication - diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java index 599fe29525..893c9ec4b4 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -56,7 +56,6 @@ import org.thingsboard.monitoring.util.ResourceUtils; import javax.security.auth.Destroyable; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -73,7 +72,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { @Setter private LeshanClient leshanClient; - private static final List supportedResources = Collections.singletonList(0); + private static final List supportedResources = List.of(0, 16); private String data = ""; @@ -158,6 +157,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { @Override public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.debug("onBootstrapFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage); // No implementation needed } @@ -245,10 +245,11 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { @Override public ReadResponse read(LwM2mServer server, int resourceId) { - if (supportedResources.contains(resourceId)) { - return ReadResponse.success(resourceId, data); - } - return super.read(server, resourceId); + return switch (resourceId) { + case 0 -> ReadResponse.success(0, data); + case 16 -> ReadResponse.success(16, "U"); + default -> super.read(server, resourceId); + }; } @SneakyThrows diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java index 57ecce2e33..0446060bb8 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java @@ -20,6 +20,7 @@ import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.elements.config.SystemConfig; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -35,6 +36,10 @@ import java.io.IOException; @Slf4j public class CoapTransportHealthChecker extends TransportHealthChecker { + static { + SystemConfig.register(); + } + private CoapClient coapClient; protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, TransportMonitoringTarget target) { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 1b1ed9bf0f..eb30ef8b9c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -556,22 +556,6 @@ public class MqttClientTest extends AbstractContainerTest { assertThat(provisionResponse.get("status").asText()).isEqualTo("NOT_FOUND"); } - @Test - public void regularDisconnect() throws Exception { - DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); - - MqttMessageListener listener = new MqttMessageListener(); - MqttClient mqttClient = getMqttClient(deviceCredentials, listener, MqttVersion.MQTT_5); - final List returnCodeByteValue = new ArrayList<>(); - MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue); - mqttClient.setCallback(callbackForDisconnectWithReturnCode); - mqttClient.disconnect(); - Thread.sleep(1000); - assertThat(returnCodeByteValue.size()).isEqualTo(1); - MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0)); - assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.NORMAL_DISCONNECT); - } - @Test public void clientSessionTakenOverDisconnect() throws Exception { DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 543553951d..801470284b 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -96,6 +96,8 @@ final class MqttClientImpl implements MqttClient { private final ListeningExecutor handlerExecutor; + private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1; + /** * Construct the MqttClientImpl with default config */ @@ -456,16 +458,25 @@ final class MqttClientImpl implements MqttClient { @Override public void disconnect() { + if (disconnected) { + return; + } + log.trace("[{}] Disconnecting from server", channel != null ? channel.id() : "UNKNOWN"); - disconnected = true; if (this.channel != null) { MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); - ChannelFuture channelFuture = this.sendAndFlushPacket(message); + + sendAndFlushPacket(message).addListener((ChannelFutureListener) future -> { + future.channel().close(); + disconnected = true; + }); eventLoop.schedule(() -> { - if (!channelFuture.isDone()) { + if (channel.isOpen()) { + log.trace("[{}] Channel still open after {} second; forcing close now", channel.id(), DISCONNECT_FALLBACK_DELAY_SECS); this.channel.close(); + disconnected = true; } - }, 500, TimeUnit.MILLISECONDS); + }, DISCONNECT_FALLBACK_DELAY_SECS, TimeUnit.SECONDS); } } diff --git a/netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java b/netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java index 1481b354ee..60e625aa8d 100644 --- a/netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java +++ b/netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java @@ -119,6 +119,26 @@ class MqttClientTest { assertThat(client.isConnected()).isTrue(); } + @Test + void testDisconnectFromBroker() { + // GIVEN + var clientConfig = new MqttClientConfig(); + clientConfig.setOwnerId("Test[Disconnect]"); + clientConfig.setClientId("disconnect"); + + client = MqttClient.create(clientConfig, null, handlerExecutor); + + connect(broker.getHost(), broker.getMqttPort()); + + // WHEN + client.disconnect(); + + // THEN + Awaitility.await("waiting for client to disconnect") + .atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> assertThat(client.isConnected()).isFalse()); + } + @Test void testDisconnectDueToKeepAliveIfNoActivity() { // GIVEN diff --git a/ui-ngx/src/app/core/http/ota-package.service.ts b/ui-ngx/src/app/core/http/ota-package.service.ts index 09f887c038..0ddbfc44de 100644 --- a/ui-ngx/src/app/core/http/ota-package.service.ts +++ b/ui-ngx/src/app/core/http/ota-package.service.ts @@ -129,15 +129,16 @@ export class OtaPackageService { } return forkJoin(tasks).pipe( mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => { - let text = ''; + const lines: string[] = []; if (deviceFirmwareUpdate > 0) { - text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}); + lines.push(this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate})); } if (deviceSoftwareUpdate > 0) { - text += text.length ? ' ' : ''; - text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate}); + lines.push(this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate})); } - return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true); + return lines.length + ? this.dialogService.confirm(this.translate.instant('ota-update.change-ota-setting-title'), lines.join('
'), null, this.translate.instant('common.proceed')) + : of(true); }) ); } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index dca6dd45eb..1511840c57 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -31,7 +31,8 @@ import { DashboardLayoutsInfo, DashboardState, DashboardStateLayouts, - GridSettings, LayoutType, + GridSettings, + LayoutType, WidgetLayout } from '@shared/models/dashboard.models'; import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; @@ -61,6 +62,7 @@ import { MediaBreakpoints } from '@shared/models/constants'; import { TranslateService } from '@ngx-translate/core'; import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models'; +import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; @Injectable({ providedIn: 'root' @@ -398,6 +400,14 @@ export class DashboardUtilsService { return datasources; } + public getWidgetDatasources(widget: Widget): Datasource[] { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.datasources(widget); + } + return this.validateAndUpdateDatasources(widget.config.datasources); + } + public createDefaultLayoutData(): DashboardLayout { return { widgets: {}, diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index cef9bbe7a3..d525bee677 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -35,7 +35,7 @@ import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.mo import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainImport } from '@shared/models/rule-chain.models'; import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models'; -import { getWidgetExportDefinition } from '@shared/models/widget/widget-export.models'; +import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; const WIDGET_ITEM = 'widget_item'; const WIDGET_REFERENCE = 'widget_reference'; @@ -142,7 +142,7 @@ export class ItemBufferService { } } let widgetExportInfo: any; - const exportDefinition = getWidgetExportDefinition(widget); + const exportDefinition = findWidgetModelDefinition(widget); if (exportDefinition) { widgetExportInfo = exportDefinition.prepareExportInfo(dashboard, widget); } @@ -270,7 +270,7 @@ export class ItemBufferService { let callFilterUpdateFunction = false; let newEntityAliases: EntityAliases; let newFilters: Filters; - const exportDefinition = getWidgetExportDefinition(widget); + const exportDefinition = findWidgetModelDefinition(widget); if (exportDefinition && widgetExportInfo || aliasesInfo) { newEntityAliases = deepClone(dashboard.configuration.entityAliases); } diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 235a41876d..f7bb87455a 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -60,7 +60,7 @@ export interface EntityAliasesDialogData { styleUrls: ['./entity-aliases-dialog.component.scss'] }) export class EntityAliasesDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements ErrorStateMatcher { title: string; disableAdd: boolean; @@ -107,8 +107,7 @@ export class EntityAliasesDialogComponent extends DialogComponent { + this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => { if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type) && datasource.entityAliasId) { this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); @@ -143,7 +142,9 @@ export class EntityAliasesDialogComponent extends DialogComponent
- -
- calculated-fields.use-message-timestamp + +
+ calculated-fields.use-latest-timestamp
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index efcd62efd4..e192e3ccc0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -45,7 +45,7 @@ &-key { color: #c24c1a; } - &-time-window, &-values, &-func, &-value, &-ts, &-msgTs { + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { color: #7214D0; } &-start-ts, &-end-ts { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 4aa4eca425..975744b2c6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -77,7 +77,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent { if (res) { diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts index 60e066fabe..048b91d25e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts @@ -14,19 +14,19 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, Inject, SkipSelf } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, + FormGroupDirective, + NgForm, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - FormGroupDirective, - NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; @@ -58,7 +58,7 @@ export interface FiltersDialogData { styleUrls: ['./filters-dialog.component.scss'] }) export class FiltersDialogComponent extends DialogComponent - implements OnInit, ErrorStateMatcher { + implements ErrorStateMatcher { title: string; disableAdd: boolean; @@ -96,15 +96,16 @@ export class FiltersDialogComponent extends DialogComponent { - const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources); - datasources.forEach((datasource) => { - if (datasource.type === DatasourceType.entity && datasource.filterId) { + this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => { + if (datasource.type !== DatasourceType.function && datasource.filterId) { widgetsTitleList = this.filterToWidgetsMap[datasource.filterId]; if (!widgetsTitleList) { widgetsTitleList = []; this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList; } - widgetsTitleList.push(widget.config.title); + if (!widgetsTitleList.includes(widget.config.title)) { + widgetsTitleList.push(widget.config.title); + } } }); }); @@ -140,9 +141,6 @@ export class FiltersDialogComponent extends DialogComponent { - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: this.isAdd}); }, 0); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss index d86864d1f3..9ce049107b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss @@ -31,4 +31,10 @@ .mat-expansion-panel-header-title { margin-right: 0; } + + &::ng-deep { + .mat-content.mat-content-hide-toggle { + margin-right: 0; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html index 06b451a1f8..63a5dfbd1f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html @@ -49,7 +49,7 @@
- diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 3eabf71899..bfe14832c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -146,6 +146,7 @@ export class WidgetActionDialogComponent extends DialogComponent
- - {{ 'tooltip.hide-zero-false-tooltip-values' | translate }} + + {{ 'tooltip.hide-zero-tooltip-values' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index c0dd95913c..bfaca01a06 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -188,7 +188,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon tooltipDateFont: [settings.tooltipDateFont, []], tooltipDateColor: [settings.tooltipDateColor, []], tooltipDateInterval: [settings.tooltipDateInterval, []], - tooltipHideZeroFalse: [settings.tooltipHideZeroFalse ,[]], + tooltipHideZeroValues: [settings.tooltipHideZeroValues ,[]], tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], @@ -265,7 +265,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont; this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor; this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval; - this.widgetConfig.config.settings.tooltipHideZeroFalse = config.tooltipHideZeroFalse; + this.widgetConfig.config.settings.tooltipHideZeroValues = config.tooltipHideZeroValues; this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; @@ -359,7 +359,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').enable(); this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable(); this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false}); - this.timeSeriesChartWidgetConfigForm.get('tooltipHideZeroFalse').enable({emitEvent: false}); + this.timeSeriesChartWidgetConfigForm.get('tooltipHideZeroValues').enable({emitEvent: false}); this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); if (tooltipShowDate) { @@ -384,7 +384,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable(); this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable(); this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable(); - this.timeSeriesChartWidgetConfigForm.get('tooltipHideZeroFalse').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipHideZeroValues').disable(); this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index b6919ec670..8458419ed8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -52,6 +52,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time- import { UserSettingsService } from '@core/http/user-settings.service'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { UtilsService } from '@core/services/utils.service'; +import { UnitService } from '@core/services/unit.service'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -92,6 +93,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid this.ctx.userSettingsService = this.$injector.get(UserSettingsService); this.ctx.utilsService = this.$injector.get(UtilsService); this.ctx.telemetryWsService = this.$injector.get(TelemetryWebsocketService); + this.ctx.unitService = this.$injector.get(UnitService); this.ctx.date = this.$injector.get(DatePipe); this.ctx.imagePipe = this.$injector.get(ImagePipe); this.ctx.milliSecondsToTimeString = this.$injector.get(MillisecondsToTimeStringPipe); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index c6b523edc6..167946b45e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -285,10 +285,10 @@ function getValueDec(ctx: WidgetContext, _settings: AnalogueGaugeSettings): numb if (ctx.data && ctx.data[0]) { dataKey = ctx.data[0].dataKey; } - if (dataKey && isDefined(dataKey.decimals)) { + if (dataKey && isDefinedAndNotNull(dataKey.decimals)) { return dataKey.decimals; } else { - return isDefinedAndNotNull(ctx.decimals) ? ctx.decimals : 0; + return ctx.decimals ?? 0; } } @@ -300,6 +300,6 @@ function getUnits(ctx: WidgetContext, settings: AnalogueGaugeSettings): TbUnit { if (dataKey?.units) { return dataKey.units; } else { - return isDefinedAndNotNull(settings.units) ? settings.units : ctx.units; + return settings.units ?? ctx.units; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index a9295e98e4..1d1257c2e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -32,7 +32,8 @@ import { ComponentStyle, getDataKey, overlayStyle, - textStyle + textStyle, + ValueFormatProcessor } from '@shared/models/widget-settings.models'; import { isDefinedAndNotNull } from '@core/utils'; import { @@ -113,11 +114,17 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn this.units = unitService.getTargetUnitSymbol(units); this.unitConvertor = unitService.geUnitConverter(units); + const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, { + units, + decimals: this.decimals, + ignoreUnitSymbol: true + }); + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; - this.rangeItems = toRangeItems(this.settings.rangeColors, this.unitConvertor); + this.rangeItems = toRangeItems(this.settings.rangeColors, valueFormat); this.visibleRangeItems = this.rangeItems.filter(item => item.visible); this.showLegend = this.settings.showLegend && !!this.rangeItems.length; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index 7134a924fe..f4210e2203 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -22,6 +22,7 @@ import { Font, simpleDateFormat, sortedColorRange, + ValueFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; @@ -291,21 +292,21 @@ export const rangeChartTimeSeriesKeySettings = (settings: RangeChartWidgetSettin } }); -export const toRangeItems = (colorRanges: Array, convertValue: (x: number) => number): RangeItem[] => { +export const toRangeItems = (colorRanges: Array, valueFormat: ValueFormatProcessor): RangeItem[] => { const rangeItems: RangeItem[] = []; let counter = 0; const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to)); for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; let from = range.from; - const to = isDefinedAndNotNull(range.to) ? convertValue(range.to) : range.to; + const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to; if (i > 0) { const prevRange = ranges[i - 1]; if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { from = prevRange.to; } } - from = isDefinedAndNotNull(from) ? convertValue(from) : from; + from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from; rangeItems.push( { index: counter++, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts index 713e7b9373..f8ead65e30 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts @@ -17,9 +17,7 @@ import { isFunction } from '@core/utils'; import { FormattedData } from '@shared/models/widget.models'; import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models'; -import { - TimeSeriesChartDataItem, -} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { TimeSeriesChartDataItem } from '@home/components/widget/lib/chart/time-series-chart.models'; import { Renderer2, SecurityContext } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { CallbackDataParams } from 'echarts/types/dist/shared'; @@ -39,7 +37,7 @@ export interface TimeSeriesChartTooltipWidgetSettings { tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction; tooltipShowDate: boolean; tooltipDateInterval?: boolean; - tooltipHideZeroFalse?: boolean; + tooltipHideZeroValues?: boolean; tooltipDateFormat: DateFormatSettings; tooltipDateFont: Font; tooltipDateColor: string; @@ -104,6 +102,9 @@ export class TimeSeriesChartTooltip { if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) { return null; } + if (this.settings.tooltipHideZeroValues && !tooltipParams.items.some(value => value.param.value[1] && value.param.value[1] !== 'false')) { + return undefined; + } const tooltipElement: HTMLElement = this.renderer.createElement('div'); this.renderer.setStyle(tooltipElement, 'display', 'flex'); @@ -130,7 +131,7 @@ export class TimeSeriesChartTooltip { this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval)); } for (const item of items) { - if (!this.settings.tooltipHideZeroFalse || item.param.value[1]) { + if (!this.settings.tooltipHideZeroValues || (item.param.value[1] && item.param.value[1] !== 'false')) { this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item)); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 81ab5d6bd9..f88be8783b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -125,6 +125,7 @@ export class TbCanvasDigitalGauge { this.barColorProcessor = ColorProcessor.fromSettings(settings.barColor, this.ctx); this.valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, { units: this.localSettings.units, + decimals: this.localSettings.decimals, ignoreUnitSymbol: true }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 7bd835de88..802668ed95 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -43,7 +43,6 @@ import { import { createLabelFromSubscriptionEntityInfo, deepClone, - formatValue, guid, isDefinedAndNotNull, isFirefox, @@ -53,12 +52,19 @@ import { isUndefinedOrNull, mergeDeep, mergeDeepIgnoreArray, + objectHashCode, parseFunction } from '@core/utils'; import { BehaviorSubject, forkJoin, Observable, Observer, of, Subject } from 'rxjs'; import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { ColorProcessor, constantColor, Font } from '@shared/models/widget-settings.models'; +import { + ColorProcessor, + constantColor, + Font, + ValueFormatProcessor, + ValueFormatSettings +} from '@shared/models/widget-settings.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetAction, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; @@ -72,10 +78,12 @@ import { FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models'; +import { TbUnit } from '@shared/models/unit.models'; export interface ScadaSymbolApi { generateElementId: () => string; - formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; + formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined; + formatValue(value: any, settings: ValueFormatSettings): string; text: (element: Element | Element[], text: string) => void; font: (element: Element | Element[], font: Font, color: string) => void; icon: (element: Element | Element[], icon: string, size?: number, color?: string, center?: boolean) => void; @@ -91,6 +99,8 @@ export interface ScadaSymbolApi { enable: (element: Element | Element[]) => void; callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; setValue: (valueId: string, value: any) => void; + unitSymbol: (unit: TbUnit) => string; + convertUnitValue: (value: any, unit: TbUnit) => number; } export interface ScadaSymbolContext { @@ -502,6 +512,8 @@ export class ScadaSymbolObject { private stateValueSubjects: {[id: string]: BehaviorSubject} = {}; + private valueProcessor: {[id: string]: ValueFormatProcessor} = {}; + private readonly shapeResize$: ResizeObserver; private readonly destroy$ = new Subject(); @@ -615,7 +627,7 @@ export class ScadaSymbolObject { this.context = { api: { generateElementId: () => generateElementId(), - formatValue, + formatValue: this.formatValue.bind(this), text: this.setElementText.bind(this), font: this.setElementFont.bind(this), icon: this.setElementIcon.bind(this), @@ -631,6 +643,8 @@ export class ScadaSymbolObject { enable: this.enableElement.bind(this), callAction: this.callAction.bind(this), setValue: this.setValue.bind(this), + unitSymbol: this.unitSymbol.bind(this), + convertUnitValue: this.convertUnitValue.bind(this), }, tags: {}, properties: {}, @@ -810,6 +824,34 @@ export class ScadaSymbolObject { } } + private unitSymbol(unit: TbUnit): string { + return this.ctx.unitService.getTargetUnitSymbol(unit); + } + + private convertUnitValue(value: number, unit: TbUnit): number { + return this.ctx.unitService.convertUnitValue(value, unit); + } + + private formatValue(value: any, settings: ValueFormatSettings): string; + private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined; + private formatValue(value: any, settingsOrDec?: ValueFormatSettings | number, units?: string, showZeroDecimals?: boolean): string { + let valueFormatSettings: ValueFormatSettings; + if (typeof settingsOrDec === 'object') { + valueFormatSettings = deepClone(settingsOrDec); + } else { + valueFormatSettings = { + units, + decimals: settingsOrDec, + showZeroDecimals + } + } + const id = objectHashCode(valueFormatSettings) + ''; + if (!this.valueProcessor[id]) { + this.valueProcessor[id] = ValueFormatProcessor.fromSettings(this.ctx.$injector, valueFormatSettings); + } + return this.valueProcessor[id].format(value); + } + private onStateValueChanged(id: string, value: any) { if (this.context.values[id] !== value) { this.context.values[id] = value; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index a433f3af02..f1554fef22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -238,8 +238,8 @@
- - {{ 'tooltip.hide-zero-false-tooltip-values' | translate }} + + {{ 'tooltip.hide-zero-tooltip-values' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 45a65b875f..cad2287745 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -163,7 +163,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon tooltipDateFont: [settings.tooltipDateFont, []], tooltipDateColor: [settings.tooltipDateColor, []], tooltipDateInterval: [settings.tooltipDateInterval, []], - tooltipHideZeroFalse: [settings.tooltipHideZeroFalse ,[]], + tooltipHideZeroValues: [settings.tooltipHideZeroValues ,[]], tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], @@ -224,7 +224,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); - this.timeSeriesChartWidgetSettingsForm.get('tooltipHideZeroFalse').enable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipHideZeroValues').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); if (tooltipShowDate) { @@ -250,7 +250,7 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').disable(); - this.timeSeriesChartWidgetSettingsForm.get('tooltipHideZeroFalse').disable(); + this.timeSeriesChartWidgetSettingsForm.get('tooltipHideZeroValues').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').disable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 8d2622ebea..4a22129ba8 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -118,6 +118,7 @@ import { CompiledTbFunction } from '@shared/models/js-function.models'; import { FormProperty } from '@shared/models/dynamic-form.models'; import { ExportableEntity } from '@shared/models/base-data'; import { TbUnit } from '@shared/models/unit.models'; +import { UnitService } from '@core/services/unit.service'; export interface IWidgetAction { name: string; @@ -225,6 +226,7 @@ export class WidgetContext { userSettingsService: UserSettingsService; utilsService: UtilsService; telemetryWsService: TelemetryWebsocketService; + unitService: UnitService; telemetrySubscribers?: Array; date: DatePipe; imagePipe: ImagePipe; diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index c7d1ddc9d0..1e4b735ff7 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef } from '@angular/core'; +import { Component, DestroyRef, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; @@ -31,7 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; templateUrl: './device-profile-tabs.component.html', styleUrls: [] }) -export class DeviceProfileTabsComponent extends EntityTabsComponent { +export class DeviceProfileTabsComponent extends EntityTabsComponent implements OnInit { deviceTransportTypes = Object.values(DeviceTransportType); @@ -55,4 +55,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponentnotification.threshold
- + notification.cpu-threshold
- + notification.ram-threshold
- + notification.storage-threshold
- + {{ 'notification.link-required' | translate }} + + {{ 'notification.link-max-length' | translate : + {length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength} + }} + {{ 'notification.subject-required' | translate }} + + {{'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength} + }} + notification.message @@ -56,6 +61,11 @@ {{ 'notification.message-required' | translate }} + + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength} + }} +
@@ -194,6 +204,11 @@ {{ 'notification.subject-required' | translate }} + + {{'notification.subject-max-length' | translate : + {length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength} + }} + = { +export const MapModelDefinition: WidgetModelDefinition = { testWidget(widget: Widget): boolean { if (widget?.config?.settings) { const settings = widget.config.settings; @@ -103,6 +104,26 @@ export const MapExportDefinition: WidgetExportDefinition = { if (info?.additionalDataSources) { updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources); } + }, + datasources(widget: Widget): Datasource[] { + const settings: BaseMapSettings = widget.config.settings as BaseMapSettings; + const datasources: Datasource[] = []; + if (settings.trips?.length) { + datasources.push(...getMapDataLayersDatasources(settings.trips)); + } + if (settings.markers?.length) { + datasources.push(...getMapDataLayersDatasources(settings.markers)); + } + if (settings.polygons?.length) { + datasources.push(...getMapDataLayersDatasources(settings.polygons)); + } + if (settings.circles?.length) { + datasources.push(...getMapDataLayersDatasources(settings.circles)); + } + if (settings.additionalDataSources?.length) { + datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); + } + return datasources; } }; @@ -189,3 +210,16 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource return null; } } + +const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => { + const datasources: Datasource[] = []; + settings.forEach((dsSettings) => { + datasources.push(mapDataSourceSettingsToDatasource(dsSettings)); + if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) { + (dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => { + datasources.push(mapDataSourceSettingsToDatasource(ds)); + }); + } + }); + return datasources; +}; diff --git a/ui-ngx/src/app/shared/models/widget/widget-export.models.ts b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts similarity index 68% rename from ui-ngx/src/app/shared/models/widget/widget-export.models.ts rename to ui-ngx/src/app/shared/models/widget/widget-model.definition.ts index 5b47010285..1b54240c09 100644 --- a/ui-ngx/src/app/shared/models/widget/widget-export.models.ts +++ b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts @@ -14,22 +14,23 @@ /// limitations under the License. /// -import { Widget } from '@shared/models/widget.models'; +import { Datasource, Widget } from '@shared/models/widget.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { EntityAliases } from '@shared/models/alias.models'; import { Filters } from '@shared/models/query/query.models'; -import { MapExportDefinition } from '@shared/models/widget/maps/map-export.models'; +import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; -export interface WidgetExportDefinition { +export interface WidgetModelDefinition { testWidget(widget: Widget): boolean; prepareExportInfo(dashboard: Dashboard, widget: Widget): T; updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; + datasources(widget: Widget): Datasource[]; } -const widgetExportDefinitions: WidgetExportDefinition[] = [ - MapExportDefinition +const widgetModelRegistry: WidgetModelDefinition[] = [ + MapModelDefinition ]; -export const getWidgetExportDefinition = (widget: Widget): WidgetExportDefinition => { - return widgetExportDefinitions.find(def => def.testWidget(widget)); +export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => { + return widgetModelRegistry.find(def => def.testWidget(widget)); } diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 5564223512..9738e9ac0f 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -7885,6 +7885,7 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, + "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { @@ -8293,6 +8294,7 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, + "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index 4c54b01499..1d45dea3c8 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1,7 +1,7 @@ ## Calculated Field TBEL Script Function The **calculate()** function is a user-defined script that enables custom calculations using [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. -It receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `msgTs` and provides access to all arguments. +It receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. ### Function Signature @@ -216,14 +216,14 @@ The return format depends on the output type configured in the calculated field ### Message timestamp -The `ctx` object also includes property `msgTs`, which represents the timestamp of the incoming telemetry message that triggered the calculated field execution in milliseconds. +The `ctx` object also includes property `latestTs`, which represents the latest timestamp of the arguments telemetry in milliseconds. -You can use `ctx.msgTs` to set the timestamp of the resulting output explicitly when returning a time series object. +You can use `ctx.latestTs` to set the timestamp of the resulting output explicitly when returning a time series object. ```javascript var temperatureC = (temperatureF - 32) / 1.8; return { - ts: ctx.msgTs, + ts: ctx.latestTs, values: { "temperatureC": toFixed(temperatureC, 2) } diff --git a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md index 9b469c0f85..6ea03a514c 100644 --- a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md +++ b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md @@ -9,8 +9,10 @@ See the available types and parameters below: Available template parameters: -* `resource` - the resource name; -* `usage` - the resource usage value; +* `resource` - the resource name (e.g., "CPU", "RAM", "STORAGE"); +* `usage` - the current usage value of the resource; +* `serviceId` - the service id (convenient in cluster setup); +* `serviceType` - the service type (convenient in cluster setup); Parameter names must be wrapped using `${...}`. For example: `${resource}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 55e136e031..a98b2aecf9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1069,7 +1069,7 @@ "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?", "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "test-with-this-message": "Test with this message", - "use-message-timestamp": "Use message timestamp", + "use-latest-timestamp": "Use latest timestamp", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", @@ -1086,7 +1086,7 @@ "decimals-range": "Decimals by default should be a number between 0 and 15.", "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.", "arguments-entity-not-found": "Argument target entity not found.", - "use-message-timestamp": "If enabled, the calculated value will be persisted using the timestamp of the telemetry that triggered the calculation, instead of the server time." + "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time." } }, "confirm-on-exit": { @@ -3442,6 +3442,7 @@ "power-button-background": "Power button background", "value-box-background": "Value box background", "value-units": "Value units", + "enable-units-scale": "Enable units on scale", "filtration-mode": "Filtration mode", "filtration-mode-hint": "Integer value indication the current filtration mode.", "filtration-mode-update": "Filtration mode update state", @@ -3963,6 +3964,7 @@ "input-fields-support-templatization": "Input fields support templatization.", "link": "Link", "link-required": "Link is required", + "link-max-length": "Link should be less than or equal to {{ length }} characters", "link-type": { "dashboard": "Open dashboard", "link": "Open URL link" @@ -4167,6 +4169,7 @@ "checksum-copied-message": "Package checksum has been copied to clipboard", "change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.", "change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.", + "change-ota-setting-title": "Are you sure you want to change OTA settings?", "chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.", "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices", "chose-software-distributed-device": "Choose software that will be distributed to the devices", @@ -5883,7 +5886,7 @@ "date": "Date", "show-date-time-interval": "Show date time interval", "show-date-time-interval-hint": "Show date time interval according to the data aggregation.", - "hide-zero-false-tooltip-values": "Hide zero/false values", + "hide-zero-tooltip-values": "Hide zero values", "background-color": "Background color", "background-blur": "Background blur" }, diff --git a/ui-ngx/src/scss/animations.scss b/ui-ngx/src/scss/animations.scss index b4f9d83d41..865e70fd35 100644 --- a/ui-ngx/src/scss/animations.scss +++ b/ui-ngx/src/scss/animations.scss @@ -16,15 +16,23 @@ @keyframes tbMoveFromTopFade { from { opacity: 0; - transform: translate(0, -100%); } + + to { + opacity: 1; + transform: translate(0, 0); + } } @keyframes tbMoveToTopFade { + from { + opacity: 1; + transform: translate(0, 0); + } + to { opacity: 0; - transform: translate(0, -100%); } } @@ -32,15 +40,23 @@ @keyframes tbMoveFromBottomFade { from { opacity: 0; - transform: translate(0, 100%); } + + to { + opacity: 1; + transform: translate(0, 0); + } } @keyframes tbMoveToBottomFade { + from { + opacity: 1; + transform: translate(0, 0); + } + to { opacity: 0; - transform: translate(0, 150%); } }