diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 81f9e6a14d..e614c9b54c 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -10,27 +10,9 @@ "externalId": null }, "metadata": { - "firstNodeIndex": 0, + "firstNodeIndex": 2, "nodes": [ { - "additionalInfo": { - "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", - "layoutX": 187, - "layoutY": 468 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - }, - "externalId": null - }, - { - "additionalInfo": { - "layoutX": 823, - "layoutY": 157 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", "configurationVersion": 1, @@ -41,13 +23,12 @@ "type": "ON_EVERY_MESSAGE" } }, - "externalId": null + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 52 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "configurationVersion": 3, @@ -60,25 +41,23 @@ "sendAttributesUpdatedNotification": false, "updateAttributesOnlyOnValueChange": true }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + } }, { - "additionalInfo": { - "layoutX": 347, - "layoutY": 149 - }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", "configuration": { "version": 0 }, - "externalId": null + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + } }, { - "additionalInfo": { - "layoutX": 825, - "layoutY": 266 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", "configuration": { @@ -86,13 +65,12 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 378 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", "configuration": { @@ -100,97 +78,92 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null - }, - { "additionalInfo": { "layoutX": 824, - "layoutY": 466 - }, + "layoutY": 378 + } + }, + { "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + } }, { - "additionalInfo": { - "layoutX": 1126, - "layoutY": 104 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "CLIENT_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 1126, + "layoutY": 104 + } }, { - "additionalInfo": { - "layoutX": 826, - "layoutY": 601 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "SERVER_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 826, + "layoutY": 601 + } } ], "connections": [ { "fromIndex": 0, - "toIndex": 3, + "toIndex": 6, "type": "Success" }, { "fromIndex": 1, - "toIndex": 7, + "toIndex": 6, "type": "Success" }, { "fromIndex": 2, - "toIndex": 7, - "type": "Success" - }, - { - "fromIndex": 3, - "toIndex": 1, + "toIndex": 0, "type": "Post telemetry" }, { - "fromIndex": 3, - "toIndex": 2, + "fromIndex": 2, + "toIndex": 1, "type": "Post attributes" }, { - "fromIndex": 3, - "toIndex": 4, + "fromIndex": 2, + "toIndex": 3, "type": "RPC Request from Device" }, { - "fromIndex": 3, - "toIndex": 5, + "fromIndex": 2, + "toIndex": 4, "type": "Other" }, { - "fromIndex": 3, - "toIndex": 6, + "fromIndex": 2, + "toIndex": 5, "type": "RPC Request to Device" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Deleted" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Updated" } ], "ruleChainConnections": null } -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/attributes_card.json b/application/src/main/data/json/system/widget_types/attributes_card.json index 6246811769..2beedd2a76 100644 --- a/application/src/main/data/json/system/widget_types/attributes_card.json +++ b/application/src/main/data/json/system/widget_types/attributes_card.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", - "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n\n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n for (var i = 0; i < self.ctx.datasources\n .length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n\n var datasourceTitleCell = $(\n '.tbDatasource-title',\n datasourceContainer);\n self.ctx.datasourceTitleCells.push(\n datasourceTitleCell);\n\n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx\n .$container);\n\n for (var a = 0; a < tbDatasource.dataKeys\n .length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n }\n\n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells\n .length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data\n .length > 0) {\n var tvPair = cellData.data[cellData.data\n .length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals ||\n cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey\n .decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(\n value, decimals, units, true);\n } else {\n txtValue = self.ctx.utilsService\n .customTranslation(value);\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n\n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height / 8;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width / 12;\n }\n datasourceTitleFontSize = Math.min(\n datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells\n .length; i++) {\n self.ctx.datasourceTitleCells[i].css(\n 'font-size', datasourceTitleFontSize +\n 'px');\n }\n var valueFontSize = self.ctx.height / 9;\n var labelFontSize = self.ctx.height / 9;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width / 15;\n labelFontSize = self.ctx.width / 15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size',\n valueFontSize + 'px');\n self.ctx.valueCells[i].css('height',\n valueFontSize * 2.5 + 'px');\n self.ctx.valueCells[i].css('padding', '0px ' +\n valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size',\n labelFontSize + 'px');\n self.ctx.labelCells[i].css('height',\n labelFontSize * 2.5 + 'px');\n self.ctx.labelCells[i].css('padding', '0px ' +\n labelFontSize + 'px');\n }\n}\n\nself.onDestroy = function() {}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\",\"decimals\":null}" @@ -29,4 +29,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/photo_camera_input.json b/application/src/main/data/json/system/widget_types/photo_camera_input.json index 69327395cc..fc676380ab 100644 --- a/application/src/main/data/json/system/widget_types/photo_camera_input.json +++ b/application/src/main/data/json/system/widget_types/photo_camera_input.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-photo-camera-input-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 8343eb918c..15779eb902 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/application/src/main/data/json/system/widget_types/timeseries_table.json @@ -17,7 +17,7 @@ "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-timeseries-table-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" }, "resources": [ { @@ -32,4 +32,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 305dc04961..8773a2d6aa 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -10,12 +10,12 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { - "layoutX": 822, - "layoutY": 294 + "layoutX": 824, + "layoutY": 156 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", @@ -30,8 +30,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 221 + "layoutX": 825, + "layoutY": 52 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", @@ -48,8 +48,8 @@ }, { "additionalInfo": { - "layoutX": 494, - "layoutY": 309 + "layoutX": 347, + "layoutY": 149 }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", @@ -59,8 +59,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 383 + "layoutX": 825, + "layoutY": 266 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", @@ -72,8 +72,8 @@ }, { "additionalInfo": { - "layoutX": 823, - "layoutY": 444 + "layoutX": 825, + "layoutY": 379 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", @@ -85,27 +85,14 @@ }, { "additionalInfo": { - "layoutX": 822, - "layoutY": 507 + "layoutX": 825, + "layoutY": 468 }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 } - }, - { - "additionalInfo": { - "description": "", - "layoutX": 209, - "layoutY": 307 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ @@ -133,11 +120,6 @@ "fromIndex": 2, "toIndex": 5, "type": "RPC Request to Device" - }, - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" } ], "ruleChainConnections": null diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index a988c9d5eb..c48dab1964 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -9,7 +9,7 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { @@ -92,27 +92,9 @@ "configuration": { "timeoutInSeconds": 60 } - }, - { - "additionalInfo": { - "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", - "layoutX": 204, - "layoutY": 240 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" - }, { "fromIndex": 2, "toIndex": 4, diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 0add4c0545..e5fdba571b 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -34,6 +34,18 @@ SET profile_data = jsonb_set( WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' THEN NULL ELSE to_jsonb(10) + END, + 'maxRelatedEntitiesToReturnPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument' + THEN NULL + ELSE to_jsonb(100) + END, + 'minAllowedDeduplicationIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(60) END ) ), @@ -43,6 +55,17 @@ WHERE NOT ( (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' AND (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + AND + (profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument' + AND + (profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF' ); -- UPDATE TENANT PROFILE CONFIGURATION END + +-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE START + +ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; +ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); + +-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b23015a1fe..35cf9cb467 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -117,6 +117,7 @@ import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; @@ -571,6 +572,10 @@ public class ActorSystemContext { @Getter private JobManager jobManager; + @Autowired + @Getter + private OwnerService ownerService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private int maxConcurrentSessionsPerDevice; @@ -659,6 +664,10 @@ public class ActorSystemContext { @Getter private long cfCalculationResultTimeout; + @Value("${actors.alarms.reevaluation_interval:120}") + @Getter + private long alarmRulesReevaluationInterval; + @Autowired @Getter private MqttClientSettings mqttClientSettings; @@ -851,8 +860,9 @@ public class ActorSystemContext { if (errorMessage != null) { eventBuilder.error(errorMessage); } - - ListenableFuture future = eventService.saveAsync(eventBuilder.build()); + CalculatedFieldDebugEvent event = eventBuilder.build(); + log.debug("Persisting calculated field debug event: {}", event); + ListenableFuture future = eventService.saveAsync(event); Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); } catch (IllegalArgumentException ex) { log.warn("Failed to persist calculated field debug message", ex); @@ -862,7 +872,7 @@ public class ActorSystemContext { private boolean checkLimits(TenantId tenantId) { if (debugModeRateLimitsConfig.isCalculatedFieldDebugPerTenantLimitsEnabled() && - !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { + !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { log.trace("[{}] Calculated field debug event limits exceeded!", tenantId); return false; } @@ -886,12 +896,13 @@ public class ActorSystemContext { return getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); } - public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { + public ScheduledFuture scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); if (delayInMs > 0) { - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + return getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); } else { ctx.tell(msg); + return null; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 27bdec2422..20cacda26a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; import java.util.Optional; @@ -89,16 +88,20 @@ public class AppActor extends ContextAwareActor { break; case PARTITION_CHANGE_MSG: case CF_PARTITIONS_CHANGE_MSG: + case CF_STATE_PARTITION_RESTORE_MSG: ctx.broadcastToChildren(msg, true); break; case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToTenantActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; case TRANSPORT_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, false); + forwardToTenantActor((TenantAwareMsg) msg, false); break; case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: @@ -108,7 +111,7 @@ public class AppActor extends ContextAwareActor { case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, true); + forwardToTenantActor((TenantAwareMsg) msg, true); break; case SESSION_TIMEOUT_MSG: ctx.broadcastToChildrenByType(msg, EntityType.TENANT); @@ -117,11 +120,11 @@ public class AppActor extends ContextAwareActor { case CF_STATE_RESTORE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -162,7 +165,7 @@ public class AppActor extends ContextAwareActor { private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { TbActorRef target = null; if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { - if (!EntityType.TENANT_PROFILE.equals(msg.getEntityId().getEntityType())) { + if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) { log.warn("Message has system tenant id: {}", msg); } } else { @@ -187,7 +190,7 @@ public class AppActor extends ContextAwareActor { } } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToTenantActor(TenantAwareMsg msg, boolean priority) { getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { if (priority) { tenantActor.tellWithHighPriority(msg); @@ -199,21 +202,6 @@ public class AppActor extends ContextAwareActor { }); } - - private void onToDeviceActorMsg(TenantAwareMsg msg, boolean priority) { - getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { - if (priority) { - tenantActor.tellWithHighPriority(msg); - } else { - tenantActor.tell(msg); - } - }, () -> { - if (msg instanceof TransportToDeviceActorMsgWrapper) { - ((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess(); - } - }); - } - private Optional getOrCreateTenantActor(TenantId tenantId) { if (deletedTenants.contains(tenantId)) { return Optional.empty(); @@ -245,6 +233,7 @@ public class AppActor extends ContextAwareActor { public TbActor createActor() { return new AppActor(context); } + } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java new file mode 100644 index 0000000000..3202296345 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; + +@Data +@Builder +public class CalculatedFieldAlarmActionMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final Alarm alarm; + private final ActionType action; + private final TbCallback callback; + + @Override + public MsgType getMsgType() { + return MsgType.CF_ALARM_ACTION_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java new file mode 100644 index 0000000000..8b5927827e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldArgumentResetMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx ctx; + private final TbCallback callback; + + @Override + public MsgType getMsgType() { + return MsgType.CF_ARGUMENT_RESET_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java new file mode 100644 index 0000000000..6fc191e3db --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; + +@Data +@Builder +public class CalculatedFieldEntityActionEventMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId entityId; + private final JsonNode entity; + private final ActionType action; + private final TbCallback callback; + + public static CalculatedFieldEntityActionEventMsg fromProto(EntityActionEventProto proto, + TbCallback callback) { + return CalculatedFieldEntityActionEventMsg.builder() + .tenantId((TenantId) ProtoUtils.fromProto(proto.getTenantId())) + .entityId(ProtoUtils.fromProto(proto.getEntityId())) + .entity(JacksonUtil.toJsonNode(proto.getEntity())) + .action(ActionType.valueOf(proto.getAction())) + .callback(callback) + .build(); + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_ENTITY_ACTION_EVENT_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index c57984ef3d..160cd995d5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -63,18 +64,33 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_STATE_RESTORE_MSG: processor.process((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.process((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_INIT_CF_MSG: processor.process((EntityInitCalculatedFieldMsg) msg); break; case CF_ENTITY_DELETE_MSG: processor.process((CalculatedFieldEntityDeleteMsg) msg); break; + case CF_RELATION_ACTION_MSG: + processor.process((CalculatedFieldRelationActionMsg) msg); + break; case CF_ENTITY_TELEMETRY_MSG: processor.process((EntityCalculatedFieldTelemetryMsg) msg); break; case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_REEVALUATE_MSG: + processor.process((CalculatedFieldReevaluateMsg) msg); + break; + case CF_ALARM_ACTION_MSG: + processor.process((CalculatedFieldAlarmActionMsg) msg); + break; + case CF_ARGUMENT_RESET_MSG: + processor.process((CalculatedFieldArgumentResetMsg) msg); + break; default: return false; } @@ -85,4 +101,5 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { void logProcessingException(Exception e) { log.warn("[{}][{}] Processing failure", tenantId, processor.entityId, e); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 0927bf6267..673db74863 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -21,10 +21,13 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -33,6 +36,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -48,6 +52,8 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; @@ -64,6 +70,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** * @author Andrew Shvayka @@ -78,7 +85,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM final CalculatedFieldProcessingService cfService; final CalculatedFieldStateService cfStateService; - TbActorCtx ctx; + TbActorCtx actorCtx; Map states = new HashMap<>(); CalculatedFieldEntityMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, EntityId entityId) { @@ -90,7 +97,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } void init(TbActorCtx ctx) { - this.ctx = ctx; + this.actorCtx = ctx; } public void stop(boolean partitionChanged) { @@ -98,8 +105,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM "[{}][{}] Stopping entity actor due to change partition event." : "[{}][{}] Stopping entity actor.", tenantId, entityId); + states.values().forEach(this::closeState); states.clear(); - ctx.stop(ctx.getSelf()); + actorCtx.stop(actorCtx.getSelf()); } public void process(CalculatedFieldPartitionChangeMsg msg) { @@ -111,28 +119,55 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM public void process(CalculatedFieldStateRestoreMsg msg) { CalculatedFieldId cfId = msg.getId().cfId(); log.debug("[{}] [{}] Processing CF state restore msg.", msg.getId().entityId(), cfId); - if (msg.getState() != null) { - states.put(cfId, msg.getState()); + CalculatedFieldState state = msg.getState(); + if (state != null) { + state.setCtx(msg.getCtx(), actorCtx); + state.setPartition(msg.getPartition()); + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + relatedEntitiesAggState.scheduleReevaluation(); + } + states.put(cfId, state); } else { - states.remove(cfId); + removeState(cfId); + } + } + + public void process(CalculatedFieldStatePartitionRestoreMsg msg) { + log.debug("Processing CF state partition restore msg: {}", msg); + for (CalculatedFieldState state : states.values()) { + if (msg.getPartition().equals(state.getPartition())) { + state.init(); + } } } public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); + log.debug("[{}] Processing entity init CF msg: {}", msg.getCtx().getCfId(), msg); var ctx = msg.getCtx(); - if (msg.isForceReinit()) { - log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); - states.remove(ctx.getCfId()); + CalculatedFieldState state; + if (msg.getStateAction() == StateAction.RECREATE) { + removeState(ctx.getCfId()); + state = null; + } else { + state = states.get(ctx.getCfId()); } try { - var state = getOrInitState(ctx); + if (state == null) { + state = createState(ctx); + } else if (msg.getStateAction() == StateAction.REINIT) { + log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); + state.reset(); + initState(state, ctx); + } else { + state.setCtx(ctx, actorCtx); + } if (state.isSizeOk()) { - processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -140,31 +175,110 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(CalculatedFieldEntityDeleteMsg msg) { + public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF argument reset msg.", entityId); + var ctx = msg.getCtx(); + try { + Map dynamicSourceArgs = ctx.getArguments().entrySet().stream() + .filter(entry -> entry.getValue().hasOwnerSource()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs); + fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + + processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, null); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + public void process(CalculatedFieldEntityDeleteMsg msg) throws CalculatedFieldException { log.debug("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { if (states.isEmpty()) { msg.getCallback().onSuccess(); } else { MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); - states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); - ctx.stop(ctx.getSelf()); + states.forEach((cfId, state) -> cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); + actorCtx.stop(actorCtx.getSelf()); } } else { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); - var state = states.remove(cfId); + var state = removeState(cfId); if (state != null) { - cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); + cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); } else { msg.getCallback().onSuccess(); } } } + public void process(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF {} related entity msg.", msg.getRelatedEntityId(), msg.getAction()); + switch (msg.getAction()) { + case UPDATED -> handleRelationUpdate(msg); + case DELETED -> handleRelationDelete(msg); + default -> msg.getCallback().onSuccess(); + } + } + + private void handleRelationUpdate(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); + var state = states.get(ctx.getCfId()); + try { + Map updatedArgs = new HashMap<>(); + if (state == null) { + state = createState(ctx); + } else { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); + updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + } + if (state.isSizeOk()) { + processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, callback); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + private void handleRelationDelete(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + CalculatedFieldId cfId = ctx.getCfId(); + CalculatedFieldState state = states.get(cfId); + if (state == null) { + msg.getCallback().onSuccess(); + return; + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + aggState.cleanupEntityData(msg.getRelatedEntityId()); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + } else { + throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + } + } else { + msg.getCallback().onSuccess(); + } + } + public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); - var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); + var numberOfCallbacks = msg.getEntityIdFields().size() + msg.getProfileIdFields().size(); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); List cfIdList = getCalculatedFieldIds(proto); Set cfIdSet = new HashSet<>(cfIdList); @@ -177,36 +291,37 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); var ctx = msg.getCtx(); - var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); + var callback = msg.getCallback(); try { List cfIds = getCalculatedFieldIds(proto); if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getAttrDataCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getRemovedTsKeysCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, msg.getEntityId(), proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getRemovedAttrKeysCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } } - private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, TbCallback callback) throws CalculatedFieldException { try { if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { processTelemetry(ctx, proto, cfIdList, callback); @@ -217,10 +332,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else if (proto.getRemovedAttrKeysCount() > 0) { processRemovedAttributes(ctx, proto, cfIdList, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF telemetry msg: {}", entityId, ctx.getCfId(), proto, e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -228,81 +344,147 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + public void process(CalculatedFieldReevaluateMsg msg) throws CalculatedFieldException { + CalculatedFieldId cfId = msg.getCtx().getCfId(); + CalculatedFieldState state = states.get(cfId); + if (state == null) { + log.debug("[{}][{}] Failed to find CF state for entity to handle {}", entityId, cfId, msg); + } else { + if (state.isSizeOk()) { + log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); + processStateIfReady(state, null, msg.getCtx(), Collections.singletonList(cfId), null, null, msg.getCallback()); + } else { + throw new RuntimeException(msg.getCtx().getSizeExceedsLimitMessage()); + } + } + } + + public void process(CalculatedFieldAlarmActionMsg msg) { + log.debug("[{}] Processing alarm action event msg: {}", entityId, msg); + for (CalculatedFieldState state : states.values()) { + if (state instanceof AlarmCalculatedFieldState alarmCfState) { + Alarm stateAlarm = alarmCfState.getCurrentAlarm(); + if (stateAlarm != null && stateAlarm.getId().equals(msg.getAlarm().getId())) { + alarmCfState.processAlarmAction(msg.getAlarm(), msg.getAction()); + } + } + } + msg.getCallback().onSuccess(); + } + + private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, MultipleTbCallback callback, + private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, TbCallback callback, Map newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { if (newArgValues.isEmpty()) { log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } CalculatedFieldState state = states.get(ctx.getCfId()); boolean justRestored = false; if (state == null) { - state = getOrInitState(ctx); + state = createState(ctx); justRestored = true; - } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) { + } else if (ctx.shouldFetchRelationQueryDynamicArgumentsFromDb(state)) { log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId()); try { Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); - var geofencingState = (GeofencingCalculatedFieldState) state; - geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING) { + var geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.updateLastDynamicArgumentsRefreshTs(); + } + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } else if (ctx.shouldFetchEntityRelations(state)) { + log.debug("[{}][{}] Going to update related entities for CF.", entityId, ctx.getCfId()); + try { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesState) { + List relatedEntities = cfService.fetchRelatedEntities(ctx, entityId); + List missingEntities = relatedEntitiesState.checkRelatedEntities(relatedEntities); + if (!missingEntities.isEmpty()) { + missingEntities.forEach(missingEntityId -> { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, missingEntityId, ctx.getArguments()); + relatedEntitiesState.updateEntityData(setEntityIdToSingleEntityArguments(missingEntityId, fetchedArgs)); + }); + justRestored = true; + } + } } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } } if (state.isSizeOk()) { - if (state.updateState(ctx, newArgValues) || justRestored) { + Map updatedArgs = state.update(newArgValues, ctx); + if (!updatedArgs.isEmpty() || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); - processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); + processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } else { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); } } - @SneakyThrows - private CalculatedFieldState getOrInitState(CalculatedFieldCtx ctx) { - CalculatedFieldState state = states.get(ctx.getCfId()); - if (state != null) { - return state; - } else { - ListenableFuture stateFuture = cfService.fetchStateFromDb(ctx, entityId); - // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. - // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. - // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, - // but this will significantly complicate the code. - state = stateFuture.get(1, TimeUnit.MINUTES); - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - states.put(ctx.getCfId(), state); - } + private CalculatedFieldState createState(CalculatedFieldCtx ctx) { + CalculatedFieldState state = createStateByType(ctx, entityId); + initState(state, ctx); return state; } - private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) { + state.setCtx(ctx, actorCtx); + state.init(); + + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isRelationQueryDynamicArguments()) { + GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.updateLastDynamicArgumentsRefreshTs(); + } + + Map arguments = fetchArguments(ctx); + state.update(arguments, ctx); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + states.put(ctx.getCfId(), state); + } + + @SneakyThrows + private Map fetchArguments(CalculatedFieldCtx ctx) { + ListenableFuture> argumentsFuture = cfService.fetchArguments(ctx, entityId); + // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. + // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. + // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, + // but this will significantly complicate the code. + return argumentsFuture.get(1, TimeUnit.MINUTES); + } + + private void processStateIfReady(CalculatedFieldState state, Map updatedArgs, CalculatedFieldCtx ctx, + List cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); + log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); + CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { @@ -312,13 +494,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.toStringOrElseNull(), null); + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.stringValue(), null); } } } else { + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); + } callback.onSuccess(); } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); } finally { if (!stateSizeChecked) { @@ -327,14 +514,30 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state.isSizeOk()) { cfStateService.persistState(ctxId, state, callback); } else { - removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); + deleteStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); } } } - private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { + private CalculatedFieldState removeState(CalculatedFieldId cfId) { + CalculatedFieldState state = states.remove(cfId); + closeState(state); + return state; + } + + private void closeState(CalculatedFieldState state) { + if (state != null) { + try { + state.close(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to close CF state", tenantId, state.getEntityId(), e); + } + } + } + + private void deleteStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { // We remove the state, but remember that it is over-sized in a local map. - cfStateService.removeState(ctxId, new TbCallback() { + cfStateService.deleteState(ctxId, new TbCallback() { @Override public void onSuccess() { callback.onFailure(ex); @@ -349,60 +552,67 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, List data) { - return mapToArguments(ctx.getMainEntityArguments(), data); + return mapToArguments(entityId, ctx.getMainEntityArguments(), Collections.emptyMap(), data); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List data) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArguments(argNames, data); + return mapToArguments(entityId, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); } - private Map mapToArguments(Map> args, List data) { - if (args.isEmpty()) { - return Collections.emptyMap(); - } + private Map mapToArguments(EntityId originator, Map> args, Map> relatedEntityArgs, List data) { Map arguments = new HashMap<>(); - for (TsKvProto item : data) { - ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); - Set argNames = args.get(key); - if (argNames != null) { - argNames.forEach(argName -> { - arguments.put(argName, new SingleValueArgumentEntry(item)); - }); - } - - key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); - argNames = args.get(key); - if (argNames != null) { - argNames.forEach(argName -> { - arguments.put(argName, new SingleValueArgumentEntry(item)); - }); + if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { + for (TsKvProto item : data) { + ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(originator, item)); + }); + } + argNames = args.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(item)); + }); + } + key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); + argNames = args.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(item)); + }); + } } } return arguments; } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList); + return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); - return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArguments(entityId, args, geofencingArgumentNames, relatedEntityArgs, scope, attrDataList); } - private Map mapToArguments(EntityId entityId, Map> args, List geofencingArgNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map> args, List geofencingArgNames, Map> relatedEntityArgs, AttributeScopeProto scope, List attrDataList) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - Set argNames = args.get(key); + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(entityId, item)); + }); + } + argNames = args.get(key); if (argNames == null) { continue; } @@ -418,23 +628,38 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List removedAttrKeys) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); - return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArgumentsWithDefaultValue(entityId, args, ctx.getArguments(), geofencingArgumentNames, relatedEntityArgs, scope, removedAttrKeys); } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) { - return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys); + return mapToArgumentsWithDefaultValue(null, ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, removedAttrKeys); } - private Map mapToArgumentsWithDefaultValue(Map> args, Map configArguments, List geofencingArgNames, AttributeScopeProto scope, List removedAttrKeys) { + private Map mapToArgumentsWithDefaultValue(EntityId msgEntityId, + Map> args, + Map configArguments, + List geofencingArgNames, + Map> relatedEntityArgs, + AttributeScopeProto scope, + List removedAttrKeys) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (String removedKey : removedAttrKeys) { ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - Set argNames = args.get(key); + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(msgEntityId, argumentEntry)); + }); + } + argNames = args.get(key); if (argNames == null) { continue; } @@ -442,28 +667,49 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (geofencingArgNames.contains(argName)) { arguments.put(argName, new GeofencingArgumentEntry()); } else { - Argument argument = configArguments.get(argName); - String defaultValue = (argument != null) ? argument.getDefaultValue() : null; - arguments.put(argName, StringUtils.isNotEmpty(defaultValue) - ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) - : new SingleValueArgumentEntry()); + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(argumentEntry)); } }); } return arguments; } - private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, List removedTelemetryKeys) { + private String getDefaultValue(Map configArguments, String argNames) { + Argument argument = configArguments.get(argNames); + return argument != null ? argument.getDefaultValue() : null; + } + + private SingleValueArgumentEntry buildSingleValue(String attrKey, String defaultValue, long ts) { + return StringUtils.isNotEmpty(defaultValue) + ? new SingleValueArgumentEntry(ts, new StringDataEntry(attrKey, defaultValue), null) + : new SingleValueArgumentEntry(); + } + + private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, EntityId entityId, List removedTelemetryKeys) { Map deletedArguments = ctx.getArguments().entrySet().stream() .filter(entry -> removedTelemetryKeys.contains(entry.getValue().getRefEntityKey().getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, deletedArguments); + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(ctx.getCfType())) { + fetchedArgs = setEntityIdToSingleEntityArguments(entityId, fetchedArgs); + } fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + return fetchedArgs; } + private Map setEntityIdToSingleEntityArguments(EntityId relatedEntityId, Map fetchedArgs) { + return fetchedArgs.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + argEntry -> new SingleValueArgumentEntry(relatedEntityId, argEntry.getValue()) + )); + } + private static List getCalculatedFieldIds(CalculatedFieldTelemetryMsgProto proto) { List cfIds = new LinkedList<>(); for (var cfId : proto.getPreviousCalculatedFieldsList()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java index 3e0fba2627..f92ed2ca9e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @Data public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSystemMsg { @@ -32,9 +31,9 @@ public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSyste private final CalculatedFieldLinkedTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_LINKED_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 7d2ae0ff44..80daff07ef 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -20,6 +20,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; @@ -70,9 +71,15 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_STATE_RESTORE_MSG: processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.onStatePartitionRestoreMsg((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_LIFECYCLE_MSG: processor.onEntityLifecycleMsg((CalculatedFieldEntityLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + processor.onEntityActionEventMsg((CalculatedFieldEntityActionEventMsg) msg); + break; case CF_TELEMETRY_MSG: processor.onTelemetryMsg((CalculatedFieldTelemetryMsg) msg); break; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 9b538a4268..3a19841cc5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -16,23 +16,39 @@ package org.thingsboard.server.actors.calculatedField; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.function.TriConsumer; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.page.PageDataIterable; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -41,10 +57,13 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.cache.TenantEntityProfileCache; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -54,10 +73,16 @@ import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Function; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -70,15 +95,20 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); + private final Map> ownerEntities = new HashMap<>(); + private ScheduledFuture cfsReevaluationTask; private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; private final CalculatedFieldService cfDaoService; private final DeviceService deviceService; private final AssetService assetService; + private final CustomerService customerService; + private final RelationService relationService; private final TbAssetProfileCache assetProfileCache; private final TbDeviceProfileCache deviceProfileCache; private final TenantEntityProfileCache entityProfileCache; + private final OwnerService ownerService; private final TbQueueCalculatedFieldSettings cfSettings; protected final TenantId tenantId; @@ -91,9 +121,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware this.cfDaoService = systemContext.getCalculatedFieldService(); this.deviceService = systemContext.getDeviceService(); this.assetService = systemContext.getAssetService(); + this.customerService = systemContext.getCustomerService(); + this.relationService = systemContext.getRelationService(); this.assetProfileCache = systemContext.getAssetProfileCache(); this.deviceProfileCache = systemContext.getDeviceProfileCache(); this.entityProfileCache = new TenantEntityProfileCache(); + this.ownerService = systemContext.getOwnerService(); this.cfSettings = systemContext.getCalculatedFieldSettings(); this.tenantId = tenantId; } @@ -104,90 +137,108 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void stop() { log.info("[{}] Stopping CF manager actor.", tenantId); - calculatedFields.values().forEach(CalculatedFieldCtx::stop); + calculatedFields.values().forEach(CalculatedFieldCtx::close); calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); + if (cfsReevaluationTask != null) { + cfsReevaluationTask.cancel(true); + cfsReevaluationTask = null; + } ctx.stop(ctx.getSelf()); } public void onCacheInitMsg(CalculatedFieldCacheInitMsg msg) { log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); - initEntityProfileCache(); + initEntitiesCache(); initCalculatedFields(); + scheduleCfsReevaluation(); msg.getCallback().onSuccess(); } public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { var cfId = msg.getId().cfId(); - var calculatedField = calculatedFields.get(cfId); + var ctx = calculatedFields.get(cfId); - if (calculatedField != null) { - if (msg.getState() != null) { - msg.getState().setRequiredArguments(calculatedField.getArgNames()); - } + if (ctx != null) { + msg.setCtx(ctx); log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tell(msg); } else { - cfStateService.removeState(msg.getId(), msg.getCallback()); + cfStateService.deleteState(msg.getId(), msg.getCallback()); } } + public void onStatePartitionRestoreMsg(CalculatedFieldStatePartitionRestoreMsg msg) { + ctx.broadcastToChildren(msg, true); + } + + private void scheduleCfsReevaluation() { + cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { + try { + calculatedFields.values().forEach(cf -> { + if (cf.isRequiresScheduledReevaluation()) { + applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { + log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); + getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); + }); + } + }); + } catch (Exception e) { + log.warn("[{}] Failed to trigger CFs reevaluation", tenantId, e); + } + }, systemContext.getAlarmRulesReevaluationInterval(), systemContext.getAlarmRulesReevaluationInterval(), TimeUnit.SECONDS); + } + public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { - log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", msg.getData().getEvent(), msg.getData().getEntityId()); - var entityType = msg.getData().getEntityId().getEntityType(); var event = msg.getData().getEvent(); + if (ComponentLifecycleEvent.RELATION_UPDATED.equals(event) || ComponentLifecycleEvent.RELATION_DELETED.equals(event)) { + log.debug("Processing relation [{}] event from entity: [{}]", event, msg.getData().getEntityId()); + onRelationChangedEvent(msg.getData(), msg.getCallback()); + return; + } + log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", event, msg.getData().getEntityId()); + var entityType = msg.getData().getEntityId().getEntityType(); switch (entityType) { - case CALCULATED_FIELD: { + case CALCULATED_FIELD -> { switch (event) { - case CREATED: - onCfCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onCfUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onCfDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onCfCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onCfUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onCfDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE: - case ASSET: { + case DEVICE, ASSET, CUSTOMER -> { switch (event) { - case CREATED: - onEntityCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onEntityUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onEntityDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onEntityCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onEntityUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onEntityDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE_PROFILE: - case ASSET_PROFILE: { + case DEVICE_PROFILE, ASSET_PROFILE -> { switch (event) { - case DELETED: - onProfileDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case DELETED -> onProfileDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - default: { - msg.getCallback().onSuccess(); + default -> msg.getCallback().onSuccess(); + } + } + + public void onEntityActionEventMsg(CalculatedFieldEntityActionEventMsg msg) { + switch (msg.getAction()) { + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + Alarm alarm = JacksonUtil.treeToValue(msg.getEntity(), Alarm.class); + CalculatedFieldAlarmActionMsg alarmActionMsg = CalculatedFieldAlarmActionMsg.builder() + .tenantId(tenantId) + .alarm(alarm) + .action(msg.getAction()) + .callback(msg.getCallback()) + .build(); + getOrCreateActor(alarm.getOriginator()).tellWithHighPriority(alarmActionMsg); } + default -> msg.getCallback().onSuccess(); } } @@ -202,6 +253,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (profileId != null) { entityProfileCache.add(profileId, entityId); } + updateEntityOwner(entityId); + if (!isMyPartition(entityId, callback)) { return; } @@ -210,8 +263,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var fieldsCount = entityIdFields.size() + profileIdFields.size(); if (fieldsCount > 0) { MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); - entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); - profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); + profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); } else { callback.onSuccess(); } @@ -230,21 +283,84 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); var entityId = msg.getEntityId(); oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback)); - newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); } else { callback.onSuccess(); } + } else if (msg.isOwnerChanged()) { + onEntityOwnerChanged(msg, callback); + } else { + callback.onSuccess(); } } private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { - entityProfileCache.removeEntityId(msg.getEntityId()); + switch (msg.getEntityId().getEntityType()) { + case DEVICE, ASSET -> entityProfileCache.removeEntityId(msg.getEntityId()); + case CUSTOMER -> ownerEntities.remove(msg.getEntityId()); + } + ownerEntities.values().forEach(entities -> entities.remove(msg.getEntityId())); if (isMyPartition(msg.getEntityId(), callback)) { log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); } } + private void onRelationChangedEvent(ComponentLifecycleMsg msg, TbCallback callback) { + Function> relationAction = switch (msg.getEvent()) { + case RELATION_UPDATED -> relatedId -> (entityId, ctx, cb) -> initRelatedEntity(entityId, relatedId, ctx, cb); + case RELATION_DELETED -> relatedId -> (entityId, ctx, cb) -> deleteRelatedEntity(entityId, relatedId, ctx, cb); + default -> null; + }; + + if (relationAction == null) { + callback.onSuccess(); + return; + } + + EntityRelation entityRelation = JacksonUtil.treeToValue(msg.getInfo(), EntityRelation.class); + EntityId toId = entityRelation.getTo(); + EntityId fromId = entityRelation.getFrom(); + String relationType = entityRelation.getType(); + + if (!(CalculatedField.isSupportedRefEntity(toId) || CalculatedField.isSupportedRefEntity(fromId))) { + callback.onSuccess(); + return; + } + + MultipleTbCallback callbackForToAndFrom = new MultipleTbCallback(2, callback); + processRelationByDirection(EntitySearchDirection.TO, relationType, toId, callbackForToAndFrom, relationAction.apply(fromId)); + processRelationByDirection(EntitySearchDirection.FROM, relationType, fromId, callbackForToAndFrom, relationAction.apply(toId)); + } + + private void processRelationByDirection(EntitySearchDirection direction, + String relationType, + EntityId mainId, + MultipleTbCallback parentCallback, + TriConsumer relationAction) { + List cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId); + if (cfsByEntityIdAndProfile.isEmpty()) { + parentCallback.onSuccess(); + return; + } + + List matchingCfs = cfsByEntityIdAndProfile.stream() + .filter(cf -> { + if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + RelationPathLevel relation = config.getRelation(); + return direction.equals(relation.direction()) && relationType.equals(relation.relationType()); + } + return false; + }) + .toList(); + + MultipleTbCallback directionCallback = new MultipleTbCallback(matchingCfs.size(), parentCallback); + + matchingCfs.forEach(ctx -> + applyToTargetCfEntityActors(ctx, directionCallback, (entityId, cb) -> relationAction.accept(entityId, ctx, cb)) + ); + } + private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); if (calculatedFields.containsKey(cfId)) { @@ -267,13 +383,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); - applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb)); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb)); } } } private CalculatedFieldCtx getCfCtx(CalculatedField cf) { - return new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService()); + return new CalculatedFieldCtx(cf, systemContext); } private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { @@ -315,12 +431,31 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware addLinks(newCf); } - var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); - if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { - applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb)); + StateAction stateAction; + if (newCfCtx.getCfType() != oldCfCtx.getCfType()) { + stateAction = StateAction.RECREATE; // completely recreate state, then calculate + } else if (newCfCtx.hasStateChanges(oldCfCtx)) { + stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate + } else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate } else { callback.onSuccess(); + return; } + + applyToTargetCfEntityActors(newCfCtx, new TbCallback() { + @Override + public void onSuccess() { + oldCfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + oldCfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb)); } } } @@ -335,14 +470,26 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); - applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); + applyToTargetCfEntityActors(cfCtx, new TbCallback() { + @Override + public void onSuccess() { + cfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + cfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); - // 2 = 1 for CF processing + 1 for links processing - MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); + // 4 = 1 for CF processing + 1 for links processing + 1 for owner entity processing + 1 for aggregation processing + MultipleTbCallback callback = new MultipleTbCallback(4, msg.getCallback()); // process all cfs related to entity, or it's profile; var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); @@ -360,6 +507,60 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } else { callback.onSuccess(); } + // process all cfs related to owner entity + if (entityId.getEntityType().isOneOf(EntityType.TENANT, EntityType.CUSTOMER)) { + List ownedEntitiesCFs = filterOwnedEntitiesCFs(msg); + if (!ownedEntitiesCFs.isEmpty()) { + cfExecService.pushMsgToLinks(msg, ownedEntitiesCFs, callback); + } else { + callback.onSuccess(); + } + } else { + callback.onSuccess(); + } + // process all aggregation cfs (if any); + List aggregationCalculatedFields = filterAggregationCfs(msg); + if (!aggregationCalculatedFields.isEmpty()) { + cfExecService.pushMsgToLinks(msg, aggregationCalculatedFields, callback); + } else { + callback.onSuccess(); + } + } + + private List filterAggregationCfs(CalculatedFieldTelemetryMsg msg) { + EntityId entityId = msg.getEntityId(); + return calculatedFields.values().stream() + .filter(cf -> CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cf.getCfType())) + .filter(cf -> cf.relatedEntityMatches(msg.getProto())) + .flatMap(cf -> findRelationsForCf(entityId, cf).stream()) + .toList(); + } + + private List findRelationsForCf(EntityId entityId, CalculatedFieldCtx cf) { + List result = new ArrayList<>(); + if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration configuration) { + RelationPathLevel relation = configuration.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) { + switch (relation.direction()) { + case FROM -> { + EntityRelation entityRelation = byRelationPathQuery.get(0); // only one supported + result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getFrom())); + } + case TO -> { + byRelationPathQuery.stream() + .filter(entityRelation -> entityRelation.getTo().equals(cf.getEntityId())) + .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo()))); + } + } + } + } + return result; } public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { @@ -382,6 +583,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private void onEntityOwnerChanged(ComponentLifecycleMsg msg, TbCallback msgCallback) { + EntityId entityId = msg.getEntityId(); + log.debug("Received changed owner msg from entity [{}]", entityId); + updateEntityOwner(entityId); + List cfs = getCalculatedFieldsByEntityIdAndProfile(entityId); + if (cfs.isEmpty()) { + msgCallback.onSuccess(); + return; + } + MultipleTbCallback callback = new MultipleTbCallback(cfs.size(), msgCallback); + cfs.forEach(cf -> { + if (isMyPartition(entityId, callback)) { + if (cf.hasCurrentOwnerSourceArguments()) { + CalculatedFieldArgumentResetMsg argResetMsg = new CalculatedFieldArgumentResetMsg(tenantId, cf, callback); + log.debug("Pushing CF argument reset msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(argResetMsg); + } else { + callback.onSuccess(); + } + } + }); + } + private List filterCalculatedFieldLinks(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); var proto = msg.getProto(); @@ -395,6 +619,27 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List filterOwnedEntitiesCFs(CalculatedFieldTelemetryMsg msg) { + Set entities = getOwnedEntities(msg.getEntityId()); + var proto = msg.getProto(); + List result = new ArrayList<>(); + for (var entityId : entities) { + var ownerEntityCFs = getCalculatedFieldsByEntityId(entityId); + for (var ctx : ownerEntityCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + var ownerEntityProfileCFs = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); + for (var ctx : ownerEntityProfileCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + } + return result; + } + private List getCalculatedFieldsByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -406,6 +651,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List getCalculatedFieldsByEntityIdAndProfile(EntityId entityId) { + List cfsByEntityIdAndProfile = new ArrayList<>(); + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(entityId)); + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(profileId)); + } + return cfsByEntityIdAndProfile; + } + private List getCalculatedFieldLinksByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -417,19 +672,40 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private Set getOwnedEntities(EntityId entityId) { + if (entityId == null) { + return Collections.emptySet(); + } + var result = ownerEntities.get(entityId); + if (result == null) { + result = Collections.emptySet(); + } + return result; + } + private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(msg); } + private void deleteRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing delete related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.DELETED, cf, callback)); + } + + private void initRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing init related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.UPDATED, cf, callback)); + } + private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { log.debug("Pushing delete CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); } - private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) { + private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, TbCallback callback) { log.debug("Pushing entity init CF msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); + getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, callback)); } private boolean isMyPartition(EntityId entityId, TbCallback callback) { @@ -447,8 +723,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private EntityId getProfileId(TenantId tenantId, EntityId entityId) { return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + case ASSET -> Optional.ofNullable(assetProfileCache.get(tenantId, (AssetId) entityId)).map(AssetProfile::getId).orElse(null); + case DEVICE -> Optional.ofNullable(deviceProfileCache.get(tenantId, (DeviceId) entityId)).map(DeviceProfile::getId).orElse(null); default -> null; }; } @@ -493,7 +769,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService()); + var cfCtx = new CalculatedFieldCtx(cf, systemContext); try { cfCtx.init(); } catch (Exception e) { @@ -512,25 +788,44 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link); } - private void initEntityProfileCache() { + private void initEntitiesCache() { PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : deviceIdInfos) { log.trace("Processing device record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process device record: {}", idInfo, e); } } + PageDataIterable assetIdInfos = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : assetIdInfos) { log.trace("Processing asset record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process asset record: {}", idInfo, e); } } + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); + for (Customer customer : customers) { + log.trace("Processing customer record: {}", customer); + try { + ownerEntities.computeIfAbsent(customer.getTenantId(), __ -> new HashSet<>()).add(customer.getId()); + } catch (Exception e) { + log.error("Failed to process customer record: {}", customer, e); + } + } + } + + private void updateEntityOwner(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + EntityId owner = ownerService.getOwner(tenantId, entityId); + ownerEntities.computeIfAbsent(owner, ownerId -> new HashSet<>()).add(entityId); } private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx, diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java new file mode 100644 index 0000000000..b617736ee0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldReevaluateMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx ctx; + + @Override + public MsgType getMsgType() { + return MsgType.CF_REEVALUATE_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java new file mode 100644 index 0000000000..4d8e1cf561 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldRelationActionMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId relatedEntityId; + private final ActionType action; + private final CalculatedFieldCtx calculatedField; + private final TbCallback callback; + + public CalculatedFieldRelationActionMsg(TenantId tenantId, + EntityId relatedEntityId, ActionType action, + CalculatedFieldCtx calculatedField, + TbCallback callback) { + this.tenantId = tenantId; + this.relatedEntityId = relatedEntityId; + this.action = action; + this.calculatedField = calculatedField; + this.callback = callback; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_RELATION_ACTION_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java index 19be7c02fa..d1c2f11aeb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java @@ -19,7 +19,9 @@ import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; @Data @@ -27,6 +29,8 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs private final CalculatedFieldEntityCtxId id; private final CalculatedFieldState state; + private final TopicPartitionInfo partition; + private CalculatedFieldCtx ctx; @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java index 68cd149cdf..a174cff268 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java @@ -31,9 +31,9 @@ public class CalculatedFieldTelemetryMsg implements ToCalculatedFieldSystemMsg { private final CalculatedFieldTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java index 1e8990ff8d..1e0025988d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java @@ -16,26 +16,29 @@ package org.thingsboard.server.actors.calculatedField; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import java.util.List; - @Data public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldCtx ctx; + private final StateAction stateAction; private final TbCallback callback; - private final boolean forceReinit; @Override public MsgType getMsgType() { return MsgType.CF_ENTITY_INIT_CF_MSG; } + + public enum StateAction { + INIT, + REINIT, + RECREATE, + REPROCESS + } } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index c143214e2b..b24b803950 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -115,14 +115,9 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; - -/** - * @author Andrew Shvayka - */ @Slf4j public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { - static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; final DeviceId deviceId; final LinkedHashMapRemoveEldest sessions; @@ -178,7 +173,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso private EdgeId findRelatedEdgeId() { List result = systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); - if (result != null && result.size() > 0) { + if (result != null && !result.isEmpty()) { EntityRelation relationToEdge = result.get(0); if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); @@ -501,7 +496,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso UUID sessionId = getSessionId(sessionInfo); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); ListenableFuture registrationFuture = systemContext.getClaimDevicesService() - .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); Futures.addCallback(registrationFuture, new FutureCallback<>() { @Override public void onSuccess(Void result) { @@ -723,7 +718,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso toDeviceRpcPendingMap.remove(requestId); status = RpcStatus.FAILED; response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " + - "attempts have been exhausted. Retry attempts set: " + maxRpcRetries); + "attempts have been exhausted. Retry attempts set: " + maxRpcRetries); } } else { md.setRetries(md.getRetries() + 1); diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 91d25a633b..35a7f01b2e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.aware.TenantAwareMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -155,6 +156,9 @@ public class TenantActor extends RuleChainManagerActor { case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToCfActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; @@ -182,11 +186,12 @@ public class TenantActor extends RuleChainManagerActor { case CF_CACHE_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + case CF_STATE_PARTITION_RESTORE_MSG: + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -194,7 +199,7 @@ public class TenantActor extends RuleChainManagerActor { return true; } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToCfActor(TenantAwareMsg msg, boolean priority) { if (cfActor == null) { if (msg instanceof CalculatedFieldStateRestoreMsg) { log.warn("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg); @@ -345,7 +350,7 @@ public class TenantActor extends RuleChainManagerActor { } } if (cfActor != null) { - if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER)) { cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } } @@ -390,6 +395,7 @@ public class TenantActor extends RuleChainManagerActor { public TbActor createActor() { return new TenantActor(context, tenantId); } + } } 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 c5b077c128..8c193a5b20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -44,6 +44,7 @@ import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -159,19 +160,27 @@ public class CalculatedFieldController extends BaseController { ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) - public PageData getCalculatedFieldsByEntityId( - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getCalculatedFieldsByEntityId(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Calculated field type. If not specified, all types will be returned.") + @RequestParam(required = false) CalculatedFieldType type, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); checkParameter("entityId", entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); - return checkNotNull(tbCalculatedFieldService.findAllByTenantIdAndEntityId(entityId, getCurrentUser(), pageLink)); + return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); } @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index b9968aefa9..82807d0762 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -164,6 +164,7 @@ public class SystemInfoController extends BaseController { systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); + systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index b9cb0c88b3..a2a7c993e9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -266,6 +266,9 @@ public class UserController extends BaseController { if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) { throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED); } + if (user.getAuthority() == Authority.TENANT_ADMIN && userService.countTenantAdmins(user.getTenantId()) == 1) { + throw new ThingsboardException("At least one tenant administrator must remain!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } tbUserService.delete(getTenantId(), getCurrentUser().getCustomerId(), user, getCurrentUser()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index fa10a49503..45145720d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -24,9 +24,11 @@ import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -36,7 +38,9 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.relation.RelationService; @@ -44,21 +48,24 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; @Data @@ -69,6 +76,7 @@ public abstract class AbstractCalculatedFieldProcessingService { protected final TimeseriesService timeseriesService; protected final ApiLimitService apiLimitService; protected final RelationService relationService; + protected final OwnerService ownerService; protected ListeningExecutorService calculatedFieldCallbackExecutor; @@ -87,32 +95,38 @@ public abstract class AbstractCalculatedFieldProcessingService { protected abstract String getExecutorNamePrefix(); - public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - Map> argFutures = switch (ctx.getCalculatedField().getType()) { - case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false); - case SIMPLE, SCRIPT -> { - Map> futures = new HashMap<>(); - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = resolveEntityId(entityId, entry.getValue()); - var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), System.currentTimeMillis()); - futures.put(entry.getKey(), argValueFuture); - } - yield futures; - } + protected ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> argFutures = switch (ctx.getCfType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false, ts); + case SIMPLE, SCRIPT, ALARM, PROPAGATION -> getBaseCalculatedFieldArguments(ctx, entityId, ts); + case RELATED_ENTITIES_AGGREGATION -> fetchRelatedEntitiesAggArguments(ctx, entityId, ts); }; - return Futures.whenAllComplete(argFutures.values()).call(() -> { - var result = createStateByType(ctx); - result.updateState(ctx, resolveArgumentFutures(argFutures)); - // TODO: move to state.init() method after merge with alarm rules 2.0 - if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { - geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); - } - return result; - }, MoreExecutors.directExecutor()); + if (ctx.getCfType() == PROPAGATION) { + argFutures.put(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId)); + } + return Futures.whenAllComplete(argFutures.values()) + .call(() -> resolveArgumentFutures(argFutures), + MoreExecutors.directExecutor()); } - protected EntityId resolveEntityId(EntityId entityId, Argument argument) { - return argument.getRefEntityId() != null ? argument.getRefEntityId() : entityId; + private Map> getBaseCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(ctx.getTenantId(), entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), ts); + futures.put(entry.getKey(), argValueFuture); + } + return futures; + } + + protected EntityId resolveEntityId(TenantId tenantId, EntityId entityId, Argument argument) { + if (argument.getRefEntityId() != null) { + return argument.getRefEntityId(); + } + if (!argument.hasOwnerSource()) { + return entityId; + } + return resolveOwnerArgument(tenantId, entityId); } protected Map resolveArgumentFutures(Map> argFutures) { @@ -132,18 +146,23 @@ public abstract class AbstractCalculatedFieldProcessingService { )); } - protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) { + protected ListenableFuture fetchPropagationCalculatedFieldArgument(CalculatedFieldCtx ctx, EntityId entityId) { + ListenableFuture> propagationEntityIds = fromDynamicSource(ctx.getTenantId(), entityId, ctx.getPropagationArgument()); + return Futures.transform(propagationEntityIds, ArgumentEntry::createPropagationArgument, MoreExecutors.directExecutor()); + } + + protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly, long startTs) { Map> argFutures = new HashMap<>(); Set> entries = ctx.getArguments().entrySet(); if (dynamicArgumentsOnly) { entries = entries.stream() - .filter(entry -> entry.getValue().hasDynamicSource()) + .filter(entry -> entry.getValue().hasRelationQuerySource()) .collect(Collectors.toSet()); } for (var entry : entries) { switch (entry.getKey()) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> - argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), System.currentTimeMillis())); + argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), startTs)); default -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> @@ -154,6 +173,41 @@ public abstract class AbstractCalculatedFieldProcessingService { return argFutures; } + protected Map> fetchRelatedEntitiesAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config)) { + return Collections.emptyMap(); + } + ListenableFuture> relatedEntitiesFut = resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()); + + return config.getArguments().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> Futures.transformAsync(relatedEntitiesFut, relatedEntities -> fetchRelatedEntitiesArgumentEntry(ctx.getTenantId(), relatedEntities, entry.getValue(), ts), MoreExecutors.directExecutor()) + )); + } + + protected ListenableFuture> resolveRelatedEntities(TenantId tenantId, EntityId entityId, RelationPathLevel relation) { + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + ListenableFuture> relationsFut = relationService.findFilteredRelationsByPathQueryAsync(tenantId, new EntityRelationPathQuery(entityId, List.of(relation)), filter); + + return Futures.transform(relationsFut, relations -> { + if (relations == null) { + return Collections.emptyList(); + } + + return switch (relation.direction()) { + case FROM -> relations.stream() + .map(EntityRelation::getTo) + .toList(); + case TO -> relations.stream() + .map(EntityRelation::getFrom) + .findFirst() + .map(List::of) + .orElseGet(Collections::emptyList); + }; + }, calculatedFieldCallbackExecutor); + } + private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Map.Entry entry) { Argument value = entry.getValue(); if (value.getRefEntityId() != null) { @@ -162,16 +216,26 @@ public abstract class AbstractCalculatedFieldProcessingService { if (!value.hasDynamicSource()) { return Futures.immediateFuture(List.of(entityId)); } + return fromDynamicSource(tenantId, entityId, value); + } + + private ListenableFuture> fromDynamicSource(TenantId tenantId, EntityId entityId, Argument value) { var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); return switch (refDynamicSourceConfiguration.getType()) { + case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId))); case RELATION_PATH_QUERY -> { var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; - yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)), + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + yield Futures.transform(relationService.findFilteredRelationsByPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId), filter), configuration::resolveEntityIds, calculatedFieldCallbackExecutor); } }; } + private EntityId resolveOwnerArgument(TenantId tenantId, EntityId entityId) { + return ownerService.getOwner(tenantId, entityId); + } + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); @@ -185,8 +249,7 @@ public abstract class AbstractCalculatedFieldProcessingService { argument.getRefEntityKey().getKey() ); return Futures.transform(attributesFuture, resultOpt -> - Map.entry(entityId, resultOpt.orElseGet(() -> - new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), + Map.entry(entityId, resultOpt.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))), calculatedFieldCallbackExecutor ); }).collect(Collectors.toList()); @@ -197,6 +260,23 @@ public abstract class AbstractCalculatedFieldProcessingService { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); } + public ListenableFuture fetchRelatedEntitiesArgumentEntry(TenantId tenantId, List aggEntities, Argument argument, long startTs) { + List>> futures = aggEntities.stream() + .map(entityId -> { + ListenableFuture argumentEntryFut = fetchArgumentValue(tenantId, entityId, argument, startTs); + return Futures.transform(argumentEntryFut, argumentEntry -> Map.entry(entityId, ArgumentEntry.createSingleValueArgument(entityId, argumentEntry)), MoreExecutors.directExecutor()); + }) + .toList(); + + ListenableFuture>> allFutures = Futures.allAsList(futures); + + return Futures.transform(allFutures, + entries -> ArgumentEntry.createAggArgument( + entries.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ), + MoreExecutors.directExecutor()); + } + protected ListenableFuture fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { return switch (argument.getRefEntityKey().getType()) { case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs); @@ -224,12 +304,12 @@ public abstract class AbstractCalculatedFieldProcessingService { return Futures.transform(attributeOptFuture, attrOpt -> { log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt); - AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, 0L)); + AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, SingleValueArgumentEntry.DEFAULT_VERSION)); return transformSingleValueArgument(Optional.of(attributeKvEntry)); }, calculatedFieldCallbackExecutor); } - protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long defaultTs) { String timeseriesKey = argument.getRefEntityKey().getKey(); log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey); return transformSingleValueArgument( @@ -237,7 +317,7 @@ public abstract class AbstractCalculatedFieldProcessingService { timeseriesService.findLatest(tenantId, entityId, timeseriesKey), result -> { log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); - return result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))); + return result.or(() -> Optional.of(new BasicTsKvEntry(defaultTs, createDefaultKvEntry(argument), SingleValueArgumentEntry.DEFAULT_VERSION))); }, calculatedFieldCallbackExecutor)); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index 70b41f069e..e673577742 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.service.cf; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.exception.TenantNotFoundException; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.exception.CalculatedFieldStateException; @@ -37,6 +42,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; +@Slf4j public abstract class AbstractCalculatedFieldStateService implements CalculatedFieldStateService { @Autowired @@ -56,25 +62,44 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF protected abstract void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback); @Override - public final void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { + public final void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { doRemove(stateId, callback); } protected abstract void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback); - protected void processRestoredState(CalculatedFieldStateProto stateMsg) { + protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition) { var id = fromProto(stateMsg.getId()); - var state = fromProto(stateMsg); - processRestoredState(id, state); + if (partition == null) { + try { + partition = actorSystemContext.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, id.tenantId(), id.entityId()); + } catch (TenantNotFoundException e) { + log.debug("Skipping CF state msg for non-existing tenant {}", id.tenantId()); + return; + } + } + var state = fromProto(id, stateMsg); + processRestoredState(id, state, partition); } - protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state) { - actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state)); + protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state, partition)); } @Override public void restore(QueueKey queueKey, Set partitions) { - stateService.update(queueKey, partitions, null); + stateService.update(queueKey, partitions, new QueueStateService.RestoreCallback() { + @Override + public void onAllPartitionsRestored() { + } + + @Override + public void onPartitionRestored(TopicPartitionInfo partition) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tellWithHighPriority(new CalculatedFieldStatePartitionRestoreMsg(partition)); + } + }); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java new file mode 100644 index 0000000000..498a215e17 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; + +@Data +@Builder +@RequiredArgsConstructor +public class AlarmCalculatedFieldResult implements CalculatedFieldResult { + + private final TbAlarmResult alarmResult; + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + TbMsgType msgType; + TbMsgMetaData metaData = new TbMsgMetaData(); + if (alarmResult.isCreated()) { + msgType = TbMsgType.ALARM_CREATED; + metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated()) { + msgType = TbMsgType.ALARM_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isSeverityUpdated()) { + msgType = TbMsgType.ALARM_SEVERITY_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); + } else { + msgType = TbMsgType.ALARM_CLEAR; + metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + if (alarmResult.getConditionRepeats() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_REPEATS, String.valueOf(alarmResult.getConditionRepeats())); + } + if (alarmResult.getConditionDuration() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_DURATION, String.valueOf(alarmResult.getConditionDuration())); + } + + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .data(JacksonUtil.toString(alarmResult.getAlarm())) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return alarmResult != null ? JacksonUtil.toString(alarmResult) : null; + } + + @Override + public boolean isEmpty() { + return alarmResult == null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java index fb63432fed..27e989de70 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; public interface CalculatedFieldCache { @@ -36,10 +38,26 @@ public interface CalculatedFieldCache { List getCalculatedFieldCtxsByEntityId(EntityId entityId); + List getAggCalculatedFieldCtxsByFilter(Predicate relatedEntityFilter); + + boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter); + void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void updateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void evict(CalculatedFieldId calculatedFieldId); + EntityId getProfileId(TenantId tenantId, EntityId entityId); + + Set getDynamicEntities(TenantId tenantId, EntityId entityId); + + void updateOwnerEntity(TenantId tenantId, EntityId entityId); + + void addOwnerEntity(TenantId tenantId, EntityId entityId); + + void evictEntity(EntityId entityId); + + void evictOwner(EntityId owner); + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 86ed174485..15988c4b7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -25,20 +25,21 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import java.util.List; import java.util.Map; public interface CalculatedFieldProcessingService { - ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); + ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId); Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId); + Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); - void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List cfIds, TbCallback callback); + void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index c779c27419..c62d5dc6d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,27 +15,18 @@ */ package org.thingsboard.server.service.cf; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbMsg; -@Data -public final class CalculatedFieldResult { +import java.util.List; - private final OutputType type; - private final AttributeScope scope; - private final JsonNode result; +public interface CalculatedFieldResult { - public boolean isEmpty() { - return result == null || result.isMissingNode() || result.isNull() || - (result.isObject() && result.isEmpty()) || - (result.isArray() && result.isEmpty()) || - (result.isTextual() && result.asText().isEmpty()); - } + TbMsg toTbMsg(EntityId entityId, List cfIds); - public String toStringOrElseNull() { - return result == null ? null : result.toString(); - } + String stringValue(); + + boolean isEmpty(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java index d0b34f18e8..10276ac421 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java @@ -33,7 +33,7 @@ public interface CalculatedFieldStateService { void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) throws CalculatedFieldStateException; - void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); + void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback); void restore(QueueKey queueKey, Set partitions); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index dfe30a0e55..e802bd7677 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -19,29 +19,36 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.page.PageDataIterable; import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; @Service @Slf4j @@ -51,9 +58,11 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentReferenceHashMap calculatedFieldFetchLocks = new ConcurrentReferenceHashMap<>(); private final CalculatedFieldService calculatedFieldService; - private final TbelInvokeService tbelInvokeService; - private final ApiLimitService apiLimitService; - private final RelationService relationService; + private final TbAssetProfileCache assetProfileCache; + private final TbDeviceProfileCache deviceProfileCache; + @Lazy + private final ActorSystemContext systemContext; + private final OwnerService ownerService; private final ConcurrentMap calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap> entityIdCalculatedFields = new ConcurrentHashMap<>(); @@ -61,6 +70,8 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentMap> entityIdCalculatedFieldLinks = new ConcurrentHashMap<>(); private final ConcurrentMap calculatedFieldsCtx = new ConcurrentHashMap<>(); + private final ConcurrentMap> ownerEntities = new ConcurrentHashMap<>(); + @Value("${queue.calculated_fields.init_fetch_pack_size:50000}") @Getter private int initFetchPackSize; @@ -113,7 +124,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { if (ctx == null) { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { - ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService, relationService); + ctx = new CalculatedFieldCtx(calculatedField, systemContext); calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } @@ -136,6 +147,40 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { .toList(); } + @Override + public List getAggCalculatedFieldCtxsByFilter(Predicate relatedEntityFilter) { + return calculatedFields.values().stream() + .filter(cf -> CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cf.getType())) + .map(cf -> getCalculatedFieldCtx(cf.getId())) + .filter(relatedEntityFilter) + .toList(); + } + + @Override + public boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter) { + List entityCfs = getCalculatedFieldCtxsByEntityId(entityId); + for (CalculatedFieldCtx ctx : entityCfs) { + if (filter.test(ctx)) { + return true; + } + } + + return hasCalculatedFieldsByProfile(tenantId, entityId, filter); + } + + public boolean hasCalculatedFieldsByProfile(TenantId tenantId, EntityId entityId, Predicate filter) { + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + List profileCfs = getCalculatedFieldCtxsByEntityId(profileId); + for (CalculatedFieldCtx ctx : profileCfs) { + if (filter.test(ctx)) { + return true; + } + } + } + return false; + } + @Override public void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId) { Lock lock = getFetchLock(calculatedFieldId); @@ -185,6 +230,53 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField); } + @Override + public EntityId getProfileId(TenantId tenantId, EntityId entityId) { + return switch (entityId.getEntityType()) { + case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); + case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + default -> null; + }; + } + + @Override + public Set getDynamicEntities(TenantId tenantId, EntityId entityId) { + if (entityId != null && entityId.getEntityType().isOneOf(EntityType.CUSTOMER, EntityType.TENANT)) { + return getOwnedEntities(tenantId, entityId); + } + return Collections.emptySet(); + } + + @Override + public void addOwnerEntity(TenantId tenantId, EntityId entityId) { + EntityId owner = ownerService.getOwner(tenantId, entityId); + getOwnedEntities(tenantId, owner).add(entityId); + } + + @Override + public void updateOwnerEntity(TenantId tenantId, EntityId entityId) { + evictEntity(entityId); + addOwnerEntity(tenantId, entityId); + } + + @Override + public void evictEntity(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + } + + @Override + public void evictOwner(EntityId owner) { + ownerEntities.remove(owner); + } + + private Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + return ownerEntities.computeIfAbsent(ownerId, owner -> { + Set entities = ConcurrentHashMap.newKeySet(); + entities.addAll(ownerService.getOwnedEntities(tenantId, ownerId)); + return entities; + }); + } + private Lock getFetchLock(CalculatedFieldId id) { return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 17dca5dd64..9a8afb2054 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -23,15 +23,12 @@ import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -51,15 +48,16 @@ import org.thingsboard.server.queue.util.TbRuleEngineComponent; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutionException; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -74,9 +72,10 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF TimeseriesService timeseriesService, ApiLimitService apiLimitService, RelationService relationService, + OwnerService ownerService, TbClusterService clusterService, PartitionService partitionService) { - super(attributesService, timeseriesService, apiLimitService, relationService); + super(attributesService, timeseriesService, apiLimitService, relationService, ownerService); this.clusterService = clusterService; this.partitionService = partitionService; } @@ -87,27 +86,40 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF } @Override - public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - return super.fetchStateFromDb(ctx, entityId); + public ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId) { + return super.fetchArguments(ctx, entityId, System.currentTimeMillis()); } @Override public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - // only geofencing calculated fields supports dynamic arguments scheduled updates - if (!ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { - return Map.of(); + return switch (ctx.getCfType()) { + case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); + case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); + default -> Collections.emptyMap(); + }; + } + + @Override + public List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId) { + try { + if (ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + return resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()).get(); + } + return Collections.emptyList(); + } catch (ExecutionException | InterruptedException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch related entities for entity [" + entityId + "]: " + cause.getMessage(), cause); } - return resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true)); } @Override public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); for (var entry : arguments.entrySet()) { - if (entry.getValue().hasDynamicSource()) { + if (entry.getValue().hasRelationQuerySource()) { continue; } - var argEntityId = resolveEntityId(entityId, entry.getValue()); + var argEntityId = resolveEntityId(tenantId, entityId, entry.getValue()); var argValueFuture = fetchArgumentValue(tenantId, argEntityId, entry.getValue(), System.currentTimeMillis()); argFutures.put(entry.getKey(), argValueFuture); } @@ -115,17 +127,36 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF } @Override - public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List cfIds, TbCallback callback) { + public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (!(result instanceof PropagationCalculatedFieldResult propagationCalculatedFieldResult)) { + TbMsg msg = result.toTbMsg(entityId, cfIds); + sendMsgToRuleEngine(tenantId, entityId, callback, msg); + return; + } + List propagationEntityIds = propagationCalculatedFieldResult.getPropagationEntityIds(); + if (propagationEntityIds.isEmpty()) { + callback.onSuccess(); + } + if (propagationEntityIds.size() == 1) { + EntityId propagationEntityId = propagationEntityIds.get(0); + TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); + sendMsgToRuleEngine(tenantId, propagationEntityId, callback, msg); + return; + } + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); + for (var propagationEntityId : propagationEntityIds) { + TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); + sendMsgToRuleEngine(tenantId, propagationEntityId, multipleTbCallback, msg); + } + } + + private void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) { try { - OutputType type = calculatedFieldResult.getType(); - TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST; - TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY; - TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.toStringOrElseNull()).build(); clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - callback.onSuccess(); log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg); + callback.onSuccess(); } @Override @@ -134,7 +165,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF } }); } catch (Exception e) { - log.warn("[{}][{}] Failed to push message to rule engine. CalculatedFieldResult: {}", tenantId, entityId, calculatedFieldResult, e); + log.warn("[{}][{}] Failed to push message to rule engine: {}", tenantId, entityId, msg, e); callback.onFailure(e); } } @@ -208,6 +239,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF public void onFailure(Throwable t) { callback.onFailure(t); } + } } 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 fc5d75be56..2bf605140a 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 @@ -25,10 +25,10 @@ import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -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; @@ -36,7 +36,12 @@ 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; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -45,13 +50,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.profile.TbAssetProfileCache; -import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; -import java.util.EnumSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.function.Predicate; import java.util.function.Supplier; @@ -74,14 +75,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } }; - private final TbAssetProfileCache assetProfileCache; - private final TbDeviceProfileCache deviceProfileCache; private final CalculatedFieldCache calculatedFieldCache; private final TbClusterService clusterService; - - private static final Set supportedReferencedEntities = EnumSet.of( - EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT - ); + private final RelationService relationService; @Override public void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback) { @@ -91,6 +87,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries), cf -> cf.linkMatches(entityId, entries), + cf -> cf.dynamicSourceMatches(request.getEntries()), + cf -> cf.relatedEntityMatches(entries), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -108,6 +106,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries, scope), cf -> cf.linkMatches(entityId, entries, scope), + cf -> cf.dynamicSourceMatches(request.getEntries(), request.getScope()), + cf -> cf.relatedEntityMatches(entries, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -124,6 +124,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result, scope), cf -> cf.linkMatchesAttrKeys(entityId, result, scope), + cf -> cf.matchesDynamicSourceKeys(result, request.getScope()), + cf -> cf.matchesRelatedEntityKeys(result, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -134,16 +136,21 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result), cf -> cf.linkMatchesTsKeys(entityId, result), + cf -> cf.matchesDynamicSourceKeys(result), + cf -> cf.matchesRelatedEntityKeys(result), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId, - Predicate mainEntityFilter, Predicate linkedEntityFilter, + Predicate mainEntityFilter, + Predicate linkedEntityFilter, + Predicate dynamicSourceFilter, + Predicate relatedEntityFilter, Supplier msg, FutureCallback callback) { if (EntityType.TENANT.equals(entityId.getEntityType())) { tenantId = (TenantId) entityId; } - boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter); + boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter, dynamicSourceFilter, relatedEntityFilter); if (send) { ToCalculatedFieldMsg calculatedFieldMsg = msg.get(); clusterService.pushMsgToCalculatedFields(tenantId, entityId, calculatedFieldMsg, wrap(callback)); @@ -154,25 +161,13 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } } - private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter) { - if (!supportedReferencedEntities.contains(entityId.getEntityType())) { + private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter, Predicate dynamicSourceFilter, Predicate relatedEntityFilter) { + if (!CalculatedField.isSupportedRefEntity(entityId)) { return false; } - List entityCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(entityId); - for (CalculatedFieldCtx ctx : entityCfs) { - if (filter.test(ctx)) { - return true; - } - } - EntityId profileId = getProfileId(tenantId, entityId); - if (profileId != null) { - List profileCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(profileId); - for (CalculatedFieldCtx ctx : profileCfs) { - if (filter.test(ctx)) { - return true; - } - } + if (calculatedFieldCache.hasCalculatedFields(tenantId, entityId, filter)) { + return true; } List links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId); @@ -183,15 +178,39 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } } - return false; - } + for (EntityId dynamicEntity : calculatedFieldCache.getDynamicEntities(tenantId, entityId)) { + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntity).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + EntityId dynamicEntityProfileId = calculatedFieldCache.getProfileId(tenantId, dynamicEntity); + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntityProfileId).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + } - private EntityId getProfileId(TenantId tenantId, EntityId entityId) { - return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); - default -> null; - }; + List cfCtxs = calculatedFieldCache.getAggCalculatedFieldCtxsByFilter(relatedEntityFilter); + for (CalculatedFieldCtx cfCtx : cfCtxs) { + if (cfCtx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { + RelationPathLevel relation = aggConfig.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + if (!byRelationPathQuery.isEmpty()) { + EntityId cfEntityId = cfCtx.getEntityId(); + for (EntityRelation entityRelation : byRelationPathQuery) { + EntityId relatedId = (inverseDirection == EntitySearchDirection.FROM) ? entityRelation.getTo() : entityRelation.getFrom(); + if (cfEntityId.equals(relatedId) || cfEntityId.equals(calculatedFieldCache.getProfileId(tenantId, relatedId))) { + return true; + } + } + } + } + } + + return false; } private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) { @@ -305,6 +324,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS public void onFailure(Throwable t) { callback.onFailure(t); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java new file mode 100644 index 0000000000..269d90e5e4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceInfoFilter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.HashSet; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class OwnerService { + + private final DeviceService deviceService; + private final AssetService assetService; + private final CustomerService customerService; + + public EntityId getOwner(TenantId tenantId, EntityId entityId) { + return switch (entityId.getEntityType()) { + case DEVICE -> deviceService.findDeviceById(tenantId, (DeviceId) entityId).getOwnerId(); + case ASSET -> assetService.findAssetById(tenantId, (AssetId) entityId).getOwnerId(); + case CUSTOMER -> tenantId; + default -> throw new UnsupportedOperationException(); + }; + } + + public Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + Set ownedEntities = new HashSet<>(); + if (EntityType.CUSTOMER.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId((CustomerId) ownerId).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId(tenantId, (CustomerId) ownerId, pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + } else if (EntityType.TENANT.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId((TenantId) ownerId).customerId(new CustomerId(CustomerId.NULL_UUID)).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId((TenantId) ownerId, new CustomerId(CustomerId.NULL_UUID), pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId((TenantId) ownerId, pageLink), 1000); + customers.forEach(customer -> ownedEntities.add(customer.getId())); + } + return ownedEntities; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java new file mode 100644 index 0000000000..780fd220a7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.List; + +@Data +@Builder +public final class PropagationCalculatedFieldResult implements CalculatedFieldResult { + + private final List propagationEntityIds; + private final TelemetryCalculatedFieldResult result; + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + return result.toTbMsg(entityId, cfIds); + } + + @Override + public String stringValue() { + return result.stringValue(); + } + + @Override + public boolean isEmpty() { + return CollectionsUtil.isEmpty(propagationEntityIds) || result.isEmpty(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java new file mode 100644 index 0000000000..e71e381807 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.common.data.DataConstants.SCOPE; + +@Data +@Builder +public final class TelemetryCalculatedFieldResult implements CalculatedFieldResult { + + private final OutputType type; + private final AttributeScope scope; + private final JsonNode result; + + public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build(); + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + TbMsgType msgType = switch (type) { + case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST; + case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST; + }; + TbMsgMetaData metaData = switch (type) { + case ATTRIBUTES -> new TbMsgMetaData(Map.of(SCOPE, scope.name())); + case TIME_SERIES -> TbMsgMetaData.EMPTY; + }; + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .previousCalculatedFieldIds(cfIds) + .data(stringValue()) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return result == null ? null : result.toString(); + } + + @Override + public boolean isEmpty() { + return result == null || result.isMissingNode() || result.isNull() || + (result.isObject() && result.isEmpty()) || + (result.isArray() && result.isEmpty()) || + (result.isTextual() && result.asText().isEmpty()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 2d43883131..b331c11a47 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -22,7 +22,9 @@ import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import java.util.List; import java.util.Map; @@ -35,7 +37,9 @@ import java.util.Map; @JsonSubTypes({ @JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"), @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING"), - @JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING") + @JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING"), + @JsonSubTypes.Type(value = PropagationArgumentEntry.class, name = "PROPAGATION"), + @JsonSubTypes.Type(value = RelatedEntitiesArgumentEntry.class, name = "RELATED_ENTITIES") }) public interface ArgumentEntry { @@ -58,6 +62,10 @@ public interface ArgumentEntry { return new SingleValueArgumentEntry(kvEntry); } + static ArgumentEntry createSingleValueArgument(EntityId entityId, ArgumentEntry argumentEntry) { + return new SingleValueArgumentEntry(entityId, argumentEntry); + } + static ArgumentEntry createTsRollingArgument(List kvEntries, int limit, long timeWindow) { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } @@ -66,4 +74,12 @@ public interface ArgumentEntry { return new GeofencingArgumentEntry(entityIdkvEntryMap); } + static ArgumentEntry createPropagationArgument(List entityIds) { + return new PropagationArgumentEntry(entityIds); + } + + static ArgumentEntry createAggArgument(Map entityIdkvEntryMap) { + return new RelatedEntitiesArgumentEntry(entityIdkvEntryMap, false); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java index 876bfa2a3f..427df2bf5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.service.cf.ctx.state; public enum ArgumentEntryType { - SINGLE_VALUE, TS_ROLLING, GEOFENCING + SINGLE_VALUE, TS_ROLLING, GEOFENCING, PROPAGATION, RELATED_ENTITIES } 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 6d877331bd..20f944e433 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 @@ -15,42 +15,59 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import lombok.AllArgsConstructor; -import lombok.Data; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.utils.CalculatedFieldUtils; +import java.io.Closeable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -@Data -@AllArgsConstructor -public abstract class BaseCalculatedFieldState implements CalculatedFieldState { +@Getter +public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { + protected final EntityId entityId; + protected CalculatedFieldCtx ctx; + protected TbActorRef actorCtx; protected List requiredArguments; - protected Map arguments; - protected boolean sizeExceedsLimit; + protected Map arguments = new HashMap<>(); + protected boolean sizeExceedsLimit; protected long latestTimestamp = -1; + protected ReadinessStatus readinessStatus; + + @Setter + private TopicPartitionInfo partition; - public BaseCalculatedFieldState(List requiredArguments) { - this.requiredArguments = requiredArguments; - this.arguments = new HashMap<>(); + public BaseCalculatedFieldState(EntityId entityId) { + this.entityId = entityId; } - public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1); + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.requiredArguments = ctx.getArgNames(); + this.readinessStatus = checkReadiness(requiredArguments, arguments); } @Override - public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - if (arguments == null) { - arguments = new HashMap<>(); - } + public void init() { + } - boolean stateUpdated = false; + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + Map updatedArguments = null; for (Map.Entry entry : argumentValues.entrySet()) { String key = entry.getKey(); @@ -63,26 +80,44 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { if (existingEntry == null || newEntry.isForceResetPrevious()) { validateNewEntry(key, newEntry); - arguments.put(key, newEntry); + if (existingEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + relatedEntitiesArgumentEntry.updateEntry(newEntry); + } else { + arguments.put(key, newEntry); + } entryUpdated = true; } else { entryUpdated = existingEntry.updateEntry(newEntry); } if (entryUpdated) { - stateUpdated = true; + if (updatedArguments == null) { + updatedArguments = new HashMap<>(argumentValues.size()); + } + updatedArguments.put(key, newEntry); updateLastUpdateTimestamp(newEntry); } } - return stateUpdated; + if (updatedArguments == null) { + return Collections.emptyMap(); + } + readinessStatus = checkReadiness(requiredArguments, arguments); + return updatedArguments; + } + + @Override + public void reset() { // must reset everything dependent on arguments + requiredArguments = null; + arguments.clear(); + sizeExceedsLimit = false; + latestTimestamp = -1; } @Override public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + return readinessStatus.ready(); } @Override @@ -93,7 +128,26 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - protected void validateNewEntry(String key, ArgumentEntry newEntry) {} + @Override + public void close() { + } + + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + } + + protected ObjectNode toSimpleResult(boolean useLatestTs, ObjectNode valuesNode) { + if (!useLatestTs) { + return valuesNode; + } + long latestTs = getLatestTimestamp(); + if (latestTs == -1) { + return valuesNode; + } + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", latestTs); + resultNode.set("values", valuesNode); + return resultNode; + } private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; @@ -106,4 +160,21 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } + protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + if (currentArguments == null) { + return ReadinessStatus.from(requiredArguments); + } + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } + } + return ReadinessStatus.from(emptyArguments); + } + } 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 64b09b94ec..0df8d5ebe8 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 @@ -15,48 +15,68 @@ */ package org.thingsboard.server.service.cf.ctx.state; +import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; -import net.objecthunter.exp4j.ExpressionBuilder; import org.mvel2.MVEL; +import org.thingsboard.common.util.ExpressionUtils; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldReevaluateMsg; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.BasicKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.io.Closeable; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; - -import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; +import java.util.stream.Collectors; @Data -public class CalculatedFieldCtx { +@Slf4j +public class CalculatedFieldCtx implements Closeable { private CalculatedField calculatedField; @@ -67,14 +87,21 @@ public class CalculatedFieldCtx { private final Map arguments; private final Map> mainEntityArguments; private final Map>> linkedEntityArguments; + private final Map> dynamicEntityArguments; + private final Map> relatedEntityArguments; private final List argNames; private Output output; private String expression; private boolean useLatestTs; + private boolean requiresScheduledReevaluation; + + private ActorSystemContext systemContext; private TbelInvokeService tbelInvokeService; private RelationService relationService; - private CalculatedFieldScriptEngine calculatedFieldScriptEngine; - private ThreadLocal customExpression; + private AlarmSubscriptionService alarmService; + + private Map tbelExpressions; + private Map> simpleExpressions; private boolean initialized; @@ -84,11 +111,16 @@ public class CalculatedFieldCtx { private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; - private List linkedEntityGeofencingArgumentNames; + private List linkedEntityAndCurrentOwnerGeofencingArgumentNames; + private List relatedEntityArgumentNames; private long scheduledUpdateIntervalMillis; - public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { + private Argument propagationArgument; + private boolean applyExpressionForResolvedArguments; + + public CalculatedFieldCtx(CalculatedField calculatedField, + ActorSystemContext systemContext) { this.calculatedField = calculatedField; this.cfId = calculatedField.getId(); @@ -98,20 +130,33 @@ public class CalculatedFieldCtx { this.arguments = new HashMap<>(); this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); + this.dynamicEntityArguments = new HashMap<>(); + this.relatedEntityArguments = new HashMap<>(); this.argNames = new ArrayList<>(); this.mainEntityGeofencingArgumentNames = new ArrayList<>(); - this.linkedEntityGeofencingArgumentNames = new ArrayList<>(); + this.linkedEntityAndCurrentOwnerGeofencingArgumentNames = new ArrayList<>(); + this.relatedEntityArgumentNames = new ArrayList<>(); this.output = calculatedField.getConfiguration().getOutput(); if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { this.arguments.putAll(argBasedConfig.getArguments()); for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); - if (refId == null && entry.getValue().hasDynamicSource()) { - relationQueryDynamicArguments = true; - continue; - } - if (refId == null || refId.equals(calculatedField.getEntityId())) { + if (refId == null) { + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cfType)) { + relatedEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + continue; + } + if (entry.getValue().hasRelationQuerySource()) { + relationQueryDynamicArguments = true; + continue; + } + if (entry.getValue().hasOwnerSource()) { + dynamicEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } else { + mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } + } else if (refId.equals(calculatedField.getEntityId())) { mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); } else { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()) @@ -119,6 +164,9 @@ public class CalculatedFieldCtx { } } this.argNames.addAll(arguments.keySet()); + this.relatedEntityArgumentNames = relatedEntityArguments.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toList()); if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) { this.expression = expressionBasedConfig.getExpression(); this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs(); @@ -129,59 +177,163 @@ public class CalculatedFieldCtx { mainEntityGeofencingArgumentNames.add(zoneGroupName); return; } - if (config.isLinkedCfEntitySource(entityId)) { - linkedEntityGeofencingArgumentNames.add(zoneGroupName); + if (config.isLinkedCfEntitySource(entityId) || config.hasCurrentOwnerSource()) { + linkedEntityAndCurrentOwnerGeofencingArgumentNames.add(zoneGroupName); } }); } + if (calculatedField.getConfiguration() instanceof PropagationCalculatedFieldConfiguration propagationConfig) { + propagationArgument = propagationConfig.toPropagationArgument(); + applyExpressionForResolvedArguments = propagationConfig.isApplyExpressionToResolvedArguments(); + relationQueryDynamicArguments = true; + } } if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L; } - this.tbelInvokeService = tbelInvokeService; - this.relationService = relationService; - - this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { + this.useLatestTs = aggConfig.isUseLatestTs(); + } + this.systemContext = systemContext; + this.tbelInvokeService = systemContext.getTbelInvokeService(); + this.relationService = systemContext.getRelationService(); + this.alarmService = systemContext.getAlarmService(); + + this.maxDataPointsPerRollingArg = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); // fixme why tenant profile update is not handled?? + this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; } public void init() { switch (cfType) { case SCRIPT -> { - try { - this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); - initialized = true; - } catch (Exception e) { - initialized = false; - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); - } + initTbelExpression(expression); + initialized = true; } case GEOFENCING -> initialized = true; case SIMPLE -> { - if (isValidExpression(expression)) { - this.customExpression = ThreadLocal.withInitial(() -> - new ExpressionBuilder(expression) - .functions(userDefinedFunctions) - .implicitMultiplication(true) - .variables(this.arguments.keySet()) - .build() - ); - initialized = true; - } else { - initialized = false; - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + initSimpleExpression(expression); + initialized = true; + } + case ALARM -> { + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getAllRules().map(rule -> rule.getValue().getCondition().getExpression()) + .forEach(expression -> { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + initTbelExpression(tbelExpression.getExpression()); + } + }); + initialized = true; + } + case PROPAGATION -> { + if (applyExpressionForResolvedArguments) { + initTbelExpression(expression); } + initialized = true; + } + case RELATED_ENTITIES_AGGREGATION -> { + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getMetrics().forEach((key, metric) -> { + if (metric.getInput() instanceof AggFunctionInput functionInput) { + initTbelExpression(functionInput.getFunction()); + } + String filter = metric.getFilter(); + if (filter != null && !filter.isEmpty()) { + initTbelExpression(filter); + } + }); + initialized = true; } } } - public void stop() { - if (calculatedFieldScriptEngine != null) { - calculatedFieldScriptEngine.destroy(); + public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { + for (Map.Entry entry : state.getArguments().entrySet()) { + try { + BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); + double value = switch (kvEntry.getDataType()) { + case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); + case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); + case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); + case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); + }; + expression.setVariable(entry.getKey(), value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); + } } - if (customExpression != null) { - customExpression.remove(); + return expression.evaluate(); + } + + public ListenableFuture evaluateTbelExpression(String expression, CalculatedFieldState state) { + return evaluateTbelExpression(tbelExpressions.get(expression), state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, CalculatedFieldState state) { + return evaluateTbelExpression(expression, state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(String expression, Map entries, long latestTimestamp) { + return evaluateTbelExpression(tbelExpressions.get(expression), entries, latestTimestamp); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, Map entries, long latestTimestamp) { + Map arguments = new LinkedHashMap<>(); + List args = new ArrayList<>(argNames.size() + 1); + args.add(new Object()); // first element is a ctx, but we will set it later; + for (String argName : argNames) { + var arg = toTbelArgument(argName, entries); + arguments.put(argName, arg); + if (arg instanceof TbelCfSingleValueArg svArg) { + args.add(svArg.getValue()); + } else { + args.add(arg); + } + } + args.set(0, new TbelCfCtx(arguments, latestTimestamp)); + + return expression.executeScriptAsync(args.toArray()); + } + + public ScheduledFuture scheduleReevaluation(long delayMs, TbActorRef actorCtx) { + log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs); + return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs); + } + + private TbelCfArg toTbelArgument(String key, Map arguments) { + return arguments.get(key).toTbelCfArg(); + } + + private void initTbelExpression(String expression) { + if (tbelExpressions == null) { + tbelExpressions = new HashMap<>(); + } else if (tbelExpressions.containsKey(expression)) { + return; + } + try { + CalculatedFieldScriptEngine engine = initEngine(tenantId, expression, tbelInvokeService); + tbelExpressions.put(expression, engine); + } catch (Exception e) { + initialized = false; + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); + } + } + + private void initSimpleExpression(String expression) { + if (simpleExpressions == null) { + simpleExpressions = new HashMap<>(); + } else if (simpleExpressions.containsKey(expression)) { + return; + } + if (isValidExpression(expression)) { + ThreadLocal compiledExpression = ThreadLocal.withInitial(() -> + ExpressionUtils.createExpression(expression, this.arguments.keySet()) + ); + simpleExpressions.put(expression, compiledExpression); + } else { + initialized = false; + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); } } @@ -228,6 +380,14 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeries(map, values); } + public boolean dynamicSourceMatches(List values) { + return matchesTimeSeries(dynamicEntityArguments, values); + } + + public boolean dynamicSourceMatches(List values, AttributeScope scope) { + return matchesAttributes(dynamicEntityArguments, values, scope); + } + private boolean matchesAttributes(Map> argMap, List values, AttributeScope scope) { if (argMap.isEmpty() || values.isEmpty()) { return false; @@ -271,6 +431,14 @@ public class CalculatedFieldCtx { return matchesTimeSeriesKeys(mainEntityArguments, keys); } + public boolean matchesDynamicSourceKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(dynamicEntityArguments, keys, scope); + } + + public boolean matchesDynamicSourceKeys(List keys) { + return matchesTimeSeriesKeys(dynamicEntityArguments, keys); + } + private boolean matchesAttributesKeys(Map> argMap, List keys, AttributeScope scope) { if (argMap.isEmpty() || keys.isEmpty()) { return false; @@ -317,6 +485,60 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeriesKeys(map, keys); } + public boolean relatedEntityMatches(List values) { + return matchesTimeSeries(relatedEntityArguments, values); + } + + public boolean relatedEntityMatches(List values, AttributeScope scope) { + return matchesAttributes(relatedEntityArguments, values, scope); + } + + public boolean matchesRelatedEntityKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(relatedEntityArguments, keys, scope); + } + + public boolean matchesRelatedEntityKeys(List keys) { + return matchesTimeSeriesKeys(relatedEntityArguments, keys); + } + + public boolean relatedEntityMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesRelatedEntityKeys(proto.getRemovedTsKeysList()); + } else { + return matchesRelatedEntityKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + + public boolean dynamicSourceMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesDynamicSourceKeys(proto.getRemovedTsKeysList()); + } else { + return matchesDynamicSourceKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + public boolean linkMatches(EntityId entityId, CalculatedFieldTelemetryMsgProto proto) { if (!proto.getTsDataList().isEmpty()) { List updatedTelemetry = proto.getTsDataList().stream() @@ -336,51 +558,163 @@ public class CalculatedFieldCtx { } } + public Map> getLinkedAndDynamicArgs(EntityId entityId) { + var argNames = new HashMap>(); + var linkedArgNames = linkedEntityArguments.get(entityId); + if (linkedArgNames != null && !linkedArgNames.isEmpty()) { + argNames.putAll(linkedArgNames); + } + if (dynamicEntityArguments != null && !dynamicEntityArguments.isEmpty()) { + argNames.putAll(dynamicEntityArguments); + } + return argNames; + } + public CalculatedFieldEntityCtxId toCalculatedFieldEntityCtxId() { return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); } - public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { - boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); - boolean outputChanged = !output.equals(other.output); - boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis; - return expressionChanged || outputChanged || scheduledUpdatesConfigChanged; + public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly + if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { + return true; + } + if (!Objects.equals(output, other.output)) { + return true; + } + if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig + && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + if (!calculatedField.getName().equals(other.getCalculatedField().getName())) { + return true; + } + + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, AlarmRule::equals)) { + // if the rules have any changes not tracked by hasStateChanges + return true; + } + } + if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { + return true; + } + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig + && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()))) { + return true; + } + return false; } public boolean hasStateChanges(CalculatedFieldCtx other) { - boolean typeChanged = !cfType.equals(other.cfType); - boolean argumentsChanged = !arguments.equals(other.arguments); - boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other); - return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged; + if (!arguments.equals(other.arguments)) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, (thisRule, otherRule) -> { + return thisRule.getCondition().getType() == otherRule.getCondition().getType(); + })) { + // reinitializing only if the rule list changed, or if a condition type changed for any rule + return true; + } + } + if (hasGeofencingZoneGroupConfigurationChanges(other)) { + return true; + } + if (hasRelatedEntitiesAggregationConfigurationChanges(other)) { + return true; + } + return false; } private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); } return false; } - public boolean hasRelationQueryDynamicArguments() { - return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1; + private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getRelation().equals(otherConfig.getRelation()); + } + return false; + } + + private boolean isScheduledUpdateEnabled() { + return scheduledUpdateIntervalMillis != -1; } - public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) { - if (!hasRelationQueryDynamicArguments()) { + public boolean shouldFetchRelationQueryDynamicArgumentsFromDb(CalculatedFieldState state) { + if (!relationQueryDynamicArguments) { return false; } - if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) { + return switch (cfType) { + case PROPAGATION -> true; + case GEOFENCING -> { + if (!isScheduledUpdateEnabled()) { + yield false; + } + var geofencingState = (GeofencingCalculatedFieldState) state; + if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + yield true; + } + yield geofencingState.getLastDynamicArgumentsRefreshTs() < + System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + default -> false; + }; + } + + public boolean shouldFetchEntityRelations(CalculatedFieldState state) { + if (!(state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState)) { return false; } - if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + if (!isScheduledUpdateEnabled()) { + return false; + } + if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == -1L) { return true; } - return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + return relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + + @Override + public void close() { + try { + if (tbelExpressions != null) { + tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); + } + if (simpleExpressions != null) { + simpleExpressions.values().forEach(ThreadLocal::remove); + } + } catch (Exception e) { + log.warn("Failed to stop {}", this, e); + } } public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } + public boolean hasCurrentOwnerSourceArguments() { + return !dynamicEntityArguments.isEmpty(); + } + + @Override + public String toString() { + return "CalculatedFieldCtx{" + + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; + } + } 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 3e0964bfd2..c649ab5dbf 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 @@ -17,48 +17,63 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.io.Closeable; import java.util.List; import java.util.Map; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type" -) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), - @JsonSubTypes.Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), + @Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldState.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldState.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldState.class, name = "RELATED_ENTITIES_AGGREGATION") }) -public interface CalculatedFieldState { +public interface CalculatedFieldState extends Closeable { @JsonIgnore CalculatedFieldType getType(); + EntityId getEntityId(); + Map getArguments(); long getLatestTimestamp(); - void setRequiredArguments(List requiredArguments); + void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); + + void init(); + + Map update(Map arguments, CalculatedFieldCtx ctx); - boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); + void reset(); - ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx); + ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception; @JsonIgnore boolean isReady(); + ReadinessStatus getReadinessStatus(); + boolean isSizeExceedsLimit(); @JsonIgnore @@ -66,6 +81,10 @@ public interface CalculatedFieldState { return !isSizeExceedsLimit(); } + TopicPartitionInfo getPartition(); + + void setPartition(TopicPartitionInfo partition); + void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { @@ -79,4 +98,17 @@ public interface CalculatedFieldState { } } + record ReadinessStatus(boolean ready, String errorMsg) { + + private static final String ERROR_MESSAGE = "Required arguments are missing: "; + private static final ReadinessStatus READY = new ReadinessStatus(true, null); + + public static ReadinessStatus from(List emptyOrMissingArguments) { + if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { + return ReadinessStatus.READY; + } + return new ReadinessStatus(false, ERROR_MESSAGE + String.join(", ", emptyOrMissingArguments)); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index 2b52892744..e8174bfd57 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -43,6 +43,7 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; @@ -77,9 +78,9 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta for (TbProtoQueueMsg msg : msgs) { try { if (msg.getValue() != null) { - processRestoredState(msg.getValue()); + processRestoredState(msg.getValue(), consumerKey.partition()); } else { - processRestoredState(getStateId(msg.getHeaders()), null); + processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition()); } } catch (Throwable t) { log.error("Failed to process state message: {}", msg, t); @@ -104,6 +105,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta this.stateProducer = (TbKafkaProducerTemplate>) queueFactory.createCalculatedFieldStateProducer(); } + @Override + public void restore(QueueKey queueKey, Set partitions) { + stateService.update(queueKey, partitions, null); + } + @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index 9dc6139ca5..05bfb8b717 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import com.google.protobuf.InvalidProtocolBufferException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -64,8 +63,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS if (stateService.getPartitions().isEmpty()) { cfRocksDb.forEach((key, value) -> { try { - processRestoredState(CalculatedFieldStateProto.parseFrom(value)); - } catch (InvalidProtocolBufferException e) { + processRestoredState(CalculatedFieldStateProto.parseFrom(value), null); + } catch (Exception e) { log.error("[{}] Failed to process restored state", key, e); } }); 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 fe7dfa04d0..c52c01549f 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 @@ -15,67 +15,54 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -@Data @Slf4j -@NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { - public ScriptCalculatedFieldState(List requiredArguments) { - super(requiredArguments); + protected CalculatedFieldScriptEngine tbelExpression; + + public ScriptCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SCRIPT; + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { - Map arguments = new LinkedHashMap<>(); - List args = new ArrayList<>(ctx.getArgNames().size() + 1); - args.add(new Object()); // first element is a ctx, but we will set it later; - for (String argName : ctx.getArgNames()) { - var arg = toTbelArgument(argName); - arguments.put(argName, arg); - if (arg instanceof TbelCfSingleValueArg svArg) { - args.add(svArg.getValue()); - } else { - args.add(arg); - } - } - args.set(0, new TbelCfCtx(arguments, getLatestTimestamp())); - ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ListenableFuture resultFuture = ctx.evaluateTbelExpression(tbelExpression, this); Output output = ctx.getOutput(); return Futures.transform(resultFuture, - result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), + result -> TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(JacksonUtil.valueToTree(result)) + .build(), MoreExecutors.directExecutor() ); } - private TbelCfArg toTbelArgument(String key) { - return arguments.get(key).toTbelCfArg(); + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SCRIPT; } } 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 5a437b52f3..ab0ed26dfe 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 @@ -19,78 +19,47 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import net.objecthunter.exp4j.Expression; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.List; import java.util.Map; -@Data -@NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { - public SimpleCalculatedFieldState(List requiredArguments) { - super(requiredArguments); - } + private ThreadLocal expression; - @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SIMPLE; + public SimpleCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - protected void validateNewEntry(String key, ArgumentEntry newEntry) { - if (newEntry instanceof TsRollingArgumentEntry) { - throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + - "Rolling argument entry is not supported for simple calculated fields."); - } + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.expression = ctx.getSimpleExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { - var expr = ctx.getCustomExpression().get(); - - for (Map.Entry entry : this.arguments.entrySet()) { - try { - BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); - double value = switch (kvEntry.getDataType()) { - case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); - case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); - case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); - case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); - }; - expr.setVariable(entry.getKey(), value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); - } - } - - double expressionResult = expr.evaluate(); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + double expressionResult = ctx.evaluateSimpleExpression(expression.get(), this); Output output = ctx.getOutput(); - Object result = formatResult(expressionResult, output.getDecimalsByDefault()); + Object result = TbUtils.roundResult(expressionResult, output.getDecimalsByDefault()); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); - return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); - } - - private Object formatResult(double expressionResult, Integer decimals) { - if (decimals == null) { - return expressionResult; - } - if (decimals.equals(0)) { - return TbUtils.toInt(expressionResult); - } - return TbUtils.toFixed(expressionResult, decimals); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(outputResult) + .build()); } private JsonNode createResultJson(boolean useLatestTs, String outputName, Object result) { @@ -102,16 +71,20 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } else { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } + return toSimpleResult(useLatestTs, valuesNode); + } - long latestTs = getLatestTimestamp(); - if (useLatestTs && latestTs != -1) { - ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", latestTs); - resultNode.set("values", valuesNode); - return resultNode; - } else { - return valuesNode; + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (newEntry instanceof TsRollingArgumentEntry) { + throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + + "Rolling argument entry is not supported for simple calculated fields."); } } + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SIMPLE; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 1ceea2c621..d8e4fcf5d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -20,9 +20,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; @@ -37,11 +39,35 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; @AllArgsConstructor public class SingleValueArgumentEntry implements ArgumentEntry { - private long ts; - private BasicKvEntry kvEntryValue; - private Long version; + @Nullable + protected EntityId entityId; - private boolean forceResetPrevious; + protected long ts; + protected BasicKvEntry kvEntryValue; + protected Long version; + + protected boolean forceResetPrevious; + + public static final Long DEFAULT_VERSION = -1L; + + public SingleValueArgumentEntry(EntityId entityId, ArgumentEntry entry) { + this(entry); + this.entityId = entityId; + } + + public SingleValueArgumentEntry(ArgumentEntry entry) { + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + this.ts = singleValueArgumentEntry.ts; + this.kvEntryValue = singleValueArgumentEntry.kvEntryValue; + this.version = singleValueArgumentEntry.version; + this.forceResetPrevious = singleValueArgumentEntry.forceResetPrevious; + } + } + + public SingleValueArgumentEntry(EntityId entityId, TsKvProto entry) { + this(entry); + this.entityId = entityId; + } public SingleValueArgumentEntry(TsKvProto entry) { this.ts = entry.getTs(); @@ -51,6 +77,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.fromProto(entry.getKv()); } + public SingleValueArgumentEntry(EntityId entityId, AttributeValueProto entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(AttributeValueProto entry) { this.ts = entry.getLastUpdateTs(); if (entry.hasVersion()) { @@ -59,6 +90,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry); } + public SingleValueArgumentEntry(EntityId entityId, KvEntry entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(KvEntry entry) { if (entry instanceof TsKvEntry tsKvEntry) { this.ts = tsKvEntry.getTs(); @@ -70,6 +106,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromKvEntry(entry); } + public SingleValueArgumentEntry(EntityId entityId, long ts, BasicKvEntry kvEntryValue, Long version) { + this(ts, kvEntryValue, version); + this.entityId = entityId; + } + public SingleValueArgumentEntry(long ts, BasicKvEntry kvEntryValue, Long version) { this.ts = ts; this.kvEntryValue = kvEntryValue; @@ -93,6 +134,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { + if (isEmpty()) { + return new TbelCfSingleValueArg(ts, null); + } Object value = kvEntryValue.getValue(); if (kvEntryValue instanceof JsonDataEntry) { try { @@ -112,8 +156,10 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { - if (singleValueEntry.getTs() <= this.ts) { - return false; + if (singleValueEntry.getTs() < this.ts) { + if (!isDefaultValue()) { + return false; + } } Long newVersion = singleValueEntry.getVersion(); @@ -128,4 +174,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } return false; } + + public boolean isDefaultValue() { + return DEFAULT_VERSION.equals(this.version); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java new file mode 100644 index 0000000000..ff50cd99b6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -0,0 +1,241 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; + +import static java.util.concurrent.TimeUnit.SECONDS; + +@Slf4j +@Getter +public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState { + + @Setter + private long lastArgsRefreshTs = -1; + @Setter + private long lastMetricsEvalTs = -1; + @Setter + private long lastRelatedEntitiesRefreshTs = -1; + private long deduplicationIntervalMs = -1; + private Map metrics; + + private ScheduledFuture reevaluationFuture; + + public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + metrics = configuration.getMetrics(); + deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); + } + + @Override + public void close() { + super.close(); + if (reevaluationFuture != null) { + reevaluationFuture.cancel(true); + reevaluationFuture = null; + } + } + + @Override + public void reset() { // must reset everything dependent on arguments + super.reset(); + lastArgsRefreshTs = -1; + lastMetricsEvalTs = -1; + lastRelatedEntitiesRefreshTs = -1; + metrics = null; + } + + public void updateLastRelatedEntitiesRefreshTs() { + lastRelatedEntitiesRefreshTs = System.currentTimeMillis(); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + lastArgsRefreshTs = System.currentTimeMillis(); + return super.update(argumentValues, ctx); + } + + public List checkRelatedEntities(List relatedEntities) { + Map> entityInputs = prepareInputs(); + findOutdatedEntities(entityInputs, relatedEntities).forEach(this::cleanupEntityData); + updateLastRelatedEntitiesRefreshTs(); + return findMissingEntities(entityInputs, relatedEntities); + } + + private List findMissingEntities(Map> entityInputs, List relatedEntities) { + List missing = new ArrayList<>(); + relatedEntities.forEach(entityId -> { + if (!entityInputs.containsKey(entityId)) { + missing.add(entityId); + log.warn("[{}] Missing related entity inputs for {}", ctx.getCfId(), entityId); + } + }); + return missing; + } + + private List findOutdatedEntities(Map> entityInputs, List relatedEntities) { + List outdated = new ArrayList<>(); + entityInputs.keySet().forEach(entityId -> { + if (!relatedEntities.contains(entityId)) { + outdated.add(entityId); + log.warn("[{}] CF state keeps outdated related entity {}", ctx.getCfId(), entityId); + } + }); + return outdated; + } + + public Map updateEntityData(Map fetchedArgs) { + lastMetricsEvalTs = -1; + return update(fetchedArgs, ctx); + } + + public void cleanupEntityData(EntityId relatedEntityId) { + arguments.values().forEach(argEntry -> { + RelatedEntitiesArgumentEntry aggEntry = (RelatedEntitiesArgumentEntry) argEntry; + aggEntry.getEntityInputs().remove(relatedEntityId); + }); + lastMetricsEvalTs = -1; + lastArgsRefreshTs = System.currentTimeMillis(); + } + + public void scheduleReevaluation() { + ScheduledFuture future = ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); + if (future != null) { + reevaluationFuture = future; + } + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + boolean cfUpdated = updatedArgs != null && updatedArgs.isEmpty(); + if (shouldRecalculate() || cfUpdated) { + Output output = ctx.getOutput(); + ObjectNode aggResult = aggregateMetrics(output); + lastMetricsEvalTs = System.currentTimeMillis(); + scheduleReevaluation(); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(toSimpleResult(ctx.isUseLatestTs(), aggResult)) + .build()); + } else { + return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); + } + } + + private boolean shouldRecalculate() { + boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; + boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; + return intervalPassed && argsUpdatedDuringInterval; + } + + private Map> prepareInputs() { + Map> inputs = new HashMap<>(); + for (Map.Entry argEntry : arguments.entrySet()) { + String key = argEntry.getKey(); + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry.getValue(); + relatedEntitiesArgumentEntry.getEntityInputs().forEach((entityId, argumentEntry) -> { + inputs.computeIfAbsent(entityId, k -> new HashMap<>()).put(key, argumentEntry); + }); + } + return inputs; + } + + private ObjectNode aggregateMetrics(Output output) throws Exception { + ObjectNode aggResult = JacksonUtil.newObjectNode(); + Map> inputs = prepareInputs(); + for (Entry entry : metrics.entrySet()) { + String metricKey = entry.getKey(); + AggMetric metric = entry.getValue(); + + AggEntry aggMetricEntry = AggEntry.createAggFunction(metric.getFunction()); + aggregateMetric(metric, aggMetricEntry, inputs); + aggMetricEntry.result(output.getDecimalsByDefault()).ifPresent(result -> { + aggResult.set(metricKey, JacksonUtil.valueToTree(result)); + }); + } + return aggResult; + } + + private void aggregateMetric(AggMetric metric, AggEntry aggEntry, Map> inputs) throws Exception { + for (Map entityInputs : inputs.values()) { + if (applyAggregation(metric.getFilter(), entityInputs)) { + Object arg = resolveAggregationInput(metric.getInput(), entityInputs); + if (arg != null) { + aggEntry.update(arg); + } + } + } + } + + private boolean applyAggregation(String filter, Map entityInputs) throws Exception { + if (filter == null || filter.isEmpty()) { + return true; + } else { + Object filterResult = ctx.evaluateTbelExpression(filter, entityInputs, getLatestTimestamp()).get(); + return filterResult instanceof Boolean booleanResult && booleanResult; + } + } + + private Object resolveAggregationInput(AggInput aggInput, Map entityInputs) throws Exception { + if (aggInput instanceof AggFunctionInput functionInput) { + return ctx.evaluateTbelExpression(functionInput.getFunction(), entityInputs, getLatestTimestamp()).get(); + } else { + String inputKey = ((AggKeyInput) aggInput).getKey(); + return entityInputs.get(inputKey).getValue(); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java new file mode 100644 index 0000000000..2abe78d243 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfRelatedEntitiesArgumentValue; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Map; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +public class RelatedEntitiesArgumentEntry implements ArgumentEntry { + + private final Map entityInputs; + + private boolean forceResetPrevious; + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.RELATED_ENTITIES; + } + + @Override + public Object getValue() { + return entityInputs; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); + return true; + } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (entry.isForceResetPrevious()) { + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + return true; + } + ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); + if (argumentEntry != null) { + argumentEntry.updateEntry(singleValueArgumentEntry); + } else { + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + } + return true; + } else { + throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType()); + } + } + + @Override + public boolean isEmpty() { + return entityInputs.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + var inputs = entityInputs.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().getId(), + e -> (TbelCfSingleValueArg) e.getValue().toTbelCfArg() + )); + return new TbelCfRelatedEntitiesArgumentValue(inputs); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java new file mode 100644 index 0000000000..c4b93fd91d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AvgAggEntry.class, name = "AVG"), + @JsonSubTypes.Type(value = CountAggEntry.class, name = "COUNT"), + @JsonSubTypes.Type(value = CountUniqueAggEntry.class, name = "COUNT_UNIQUE"), + @JsonSubTypes.Type(value = MaxAggEntry.class, name = "MAX"), + @JsonSubTypes.Type(value = MinAggEntry.class, name = "MIN"), + @JsonSubTypes.Type(value = SumAggEntry.class, name = "SUM") +}) +public interface AggEntry { + + @JsonIgnore + AggFunction getType(); + + void update(Object value); + + Optional result(Integer precision); + + static AggEntry createAggFunction(AggFunction function) { + return switch (function) { + case MIN -> new MinAggEntry(); + case MAX -> new MaxAggEntry(); + case SUM -> new SumAggEntry(); + case AVG -> new AvgAggEntry(); + case COUNT -> new CountAggEntry(); + case COUNT_UNIQUE -> new CountUniqueAggEntry(); + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java new file mode 100644 index 0000000000..e063ff2ea2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class AvgAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + private long count = 0L; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + this.count++; + } + + @Override + protected Object prepareResult(Integer precision) { + double result = sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_UP).doubleValue(); + return TbUtils.roundResult(result, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.AVG; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java new file mode 100644 index 0000000000..8ca523938d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import java.util.Optional; + +public abstract class BaseAggEntry implements AggEntry { + + private boolean hasResult = false; + + @Override + public void update(Object value) { + doUpdate(extractDoubleValue(value)); + hasResult = true; + } + + @Override + public Optional result(Integer precision) { + if (hasResult) { + hasResult = false; + return Optional.of(prepareResult(precision)); + } else { + return Optional.empty(); + } + } + + protected abstract void doUpdate(double value); + + protected abstract Object prepareResult(Integer precision); + + protected double extractDoubleValue(Object value) { + try { + if (value instanceof Number number) { + return number.doubleValue(); + } + return Double.parseDouble(value.toString()); + } catch (Exception e) { + throw new NumberFormatException("Cannot parse value " + value.toString()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java new file mode 100644 index 0000000000..09116985d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +public class CountAggEntry implements AggEntry { + + private long count = 0L; + + @Override + public void update(Object value) { + count++; + } + + @Override + public Optional result(Integer precision) { + return Optional.of(count); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java new file mode 100644 index 0000000000..3c14d7c9b9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; +import java.util.Set; + +public class CountUniqueAggEntry implements AggEntry { + + private Set items; + + @Override + public void update(Object value) { + if (value != null) { + items.add(String.valueOf(value)); + } + } + + @Override + public Optional result(Integer precision) { + return Optional.of(items.size()); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT_UNIQUE; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java new file mode 100644 index 0000000000..6d734a5a08 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +public class MaxAggEntry extends BaseAggEntry { + + private double max = Double.MIN_VALUE; + + @Override + protected void doUpdate(double value) { + if (value > max) { + max = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(max, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MAX; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java new file mode 100644 index 0000000000..e517ad305f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +public class MinAggEntry extends BaseAggEntry { + + private double min = Double.MAX_VALUE; + + @Override + protected void doUpdate(double value) { + if (value < min) { + min = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(min, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MIN; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java new file mode 100644 index 0000000000..fe29d27b7e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.math.BigDecimal; + +public class SumAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(sum.doubleValue(), precision); + } + + @Override + public AggFunction getType() { + return AggFunction.SUM; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java new file mode 100644 index 0000000000..b96fde9898 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -0,0 +1,552 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.service.cf.AlarmCalculatedFieldResult; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import static org.thingsboard.server.common.data.StringUtils.equalsAny; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { + + private AlarmCalculatedFieldConfiguration configuration; + private String alarmType; + + @Getter + private final Map createRuleStates = new TreeMap<>(Comparator.comparing(Enum::ordinal)); + @Getter + @Setter + private AlarmRuleState clearRuleState; + + @Getter + private Alarm currentAlarm; + private boolean initialFetchDone; + + // TODO: deprecate device profile node, describe the differences and improvements + + public AlarmCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.configuration = getConfiguration(ctx); + this.alarmType = ctx.getCalculatedField().getName(); + + Map createRules = configuration.getCreateRules(); + createRules.forEach((severity, rule) -> { + AlarmRuleState ruleState = createRuleStates.get(severity); + if (ruleState != null) { + ruleState.setAlarmRule(rule); + } + }); + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null && clearRuleState != null) { + clearRuleState.setAlarmRule(clearRule); + } + + if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) { + currentAlarm = null; + initialFetchDone = false; + } + } + + @Override + public void init() { + super.init(); + AtomicBoolean reevalNeeded = new AtomicBoolean(false); + Map createRules = configuration.getCreateRules(); + for (AlarmSeverity severity : AlarmSeverity.values()) { + AlarmRule rule = createRules.get(severity); + if (rule != null) { + createRuleStates.compute(severity, (__, ruleState) -> { + return initRuleState(severity, rule, ruleState, reevalNeeded); + }); + } else { + AlarmRuleState state = createRuleStates.remove(severity); + if (state != null) { + clearState(state); + } + } + } + + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null) { + clearRuleState = initRuleState(null, clearRule, clearRuleState, reevalNeeded); + } else { + if (clearRuleState != null) { + clearState(clearRuleState); + clearRuleState = null; + } + } + log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, configuration); + + if (reevalNeeded.get()) { + initCurrentAlarm(ctx); + createOrClearAlarms(state -> { + if (state.getCondition().getType() == AlarmConditionType.DURATION) { + AlarmEvalResult evalResult = state.reeval(System.currentTimeMillis(), ctx); + if (evalResult.getStatus() == TRUE || evalResult.getStatus() == NOT_YET_TRUE) { + ScheduledFuture future = ctx.scheduleReevaluation(evalResult.getLeftDuration(), actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + } + return AlarmEvalResult.NOT_YET_TRUE; + }, ctx); + } + } + + private AlarmRuleState initRuleState(AlarmSeverity severity, AlarmRule rule, AlarmRuleState ruleState, AtomicBoolean reevalNeeded) { + if (ruleState == null) { + ruleState = new AlarmRuleState(severity, rule, this); + } else { + // when restored + ruleState.setAlarmRule(rule); + ruleState.setActive(null); + AlarmCondition condition = rule.getCondition(); + if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) { + reevalNeeded.set(true); + } + } + return ruleState; + } + + @Override + public void reset() { + super.reset(); + configuration = null; + } + + @Override + public void close() { + super.close(); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + clearState(clearRuleState); + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + initCurrentAlarm(ctx); + TbAlarmResult result = createOrClearAlarms(state -> { + if (updatedArgs != null) { + boolean newEvent = !updatedArgs.isEmpty(); + AlarmEvalResult evalResult = state.eval(newEvent, ctx); + if (evalResult.getStatus() == NOT_YET_TRUE && evalResult.getLeftDuration() > 0) { + long leftDuration = evalResult.getLeftDuration(); + ScheduledFuture future = ctx.scheduleReevaluation(leftDuration, actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + return evalResult; + } else { + return state.reeval(System.currentTimeMillis(), ctx); + } + }, ctx); + return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() + .alarmResult(result) + .build()); + } + + public void processAlarmAction(Alarm alarm, ActionType action) { + switch (action) { + case ALARM_ACK -> processAlarmAck(alarm); + case ALARM_CLEAR -> processAlarmClear(alarm); + case ALARM_DELETE -> processAlarmDelete(alarm); + } + } + + private void processAlarmClear(Alarm alarm) { + currentAlarm = null; + createRuleStates.values().forEach(this::clearState); + clearState(clearRuleState); + } + + private void processAlarmAck(Alarm alarm) { + currentAlarm.setAcknowledged(alarm.isAcknowledged()); + currentAlarm.setAckTs(alarm.getAckTs()); + } + + private void processAlarmDelete(Alarm alarm) { + processAlarmClear(alarm); + } + + private TbAlarmResult createOrClearAlarms(Function evalFunction, + CalculatedFieldCtx ctx) { + TbAlarmResult result = null; + AlarmRuleState resultState = null; + AlarmRuleState.StateInfo resultStateInfo = null; + + for (AlarmRuleState state : createRuleStates.values()) { + AlarmEvalResult evalResult = evalFunction.apply(state); + log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultState = state; + break; + } else if (evalResult.getStatus() == FALSE) { + clearState(state); + } + } + + if (resultState != null) { + result = calculateAlarmResult(resultState, ctx); + resultStateInfo = resultState.getStateInfo(); + log.debug("Alarm result for state {}: {}", resultState, result); + clearState(clearRuleState); + } else if (currentAlarm != null && clearRuleState != null) { + AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); + log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultStateInfo = clearRuleState.getStateInfo(); + clearState(clearRuleState); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), false + ); + if (clearResult.isCleared()) { + result = TbAlarmResult.builder() + .isCleared(true) + .alarm(clearResult.getAlarm()) + .build(); + resultState = clearRuleState; + } + currentAlarm = null; + } else if (evalResult.getStatus() == FALSE) { + clearState(clearRuleState); + } + } + if (result != null && resultState != null) { + result.setConditionRepeats(resultStateInfo.eventCount()); + result.setConditionDuration(resultStateInfo.duration()); + } + return result; + } + + private void clearState(AlarmRuleState state) { + if (state != null) { + log.debug("Clearing rule state {}", state); + state.clear(); + } + } + + private void initCurrentAlarm(CalculatedFieldCtx ctx) { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), entityId, alarmType); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + } + + private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) { + AlarmSeverity severity = ruleState.getSeverity(); + if (currentAlarm != null) { + currentAlarm.setEndTs(System.currentTimeMillis()); + AlarmSeverity oldSeverity = currentAlarm.getSeverity(); + // Skip update if severity is decreased. + if (severity.ordinal() <= oldSeverity.ordinal()) { + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } else { + return null; + } + } else { + var newAlarm = new Alarm(); + newAlarm.setType(alarmType); + newAlarm.setAcknowledged(false); + newAlarm.setCleared(false); + newAlarm.setSeverity(severity); + long startTs = latestTimestamp; + long currentTime = System.currentTimeMillis(); + if (startTs == 0L || startTs > currentTime) { + startTs = currentTime; + } + newAlarm.setStartTs(startTs); + newAlarm.setEndTs(startTs); + newAlarm.setDetails(createDetails(ruleState)); + newAlarm.setOriginator(entityId); + newAlarm.setTenantId(ctx.getTenantId()); + newAlarm.setPropagate(configuration.isPropagate()); + newAlarm.setPropagateToOwner(configuration.isPropagateToOwner()); + newAlarm.setPropagateToTenant(configuration.isPropagateToTenant()); + if (configuration.getPropagateRelationTypes() != null) { + newAlarm.setPropagateRelationTypes(configuration.getPropagateRelationTypes()); + } + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } + } + + private JsonNode createDetails(AlarmRuleState ruleState) { + JsonNode alarmDetails; + String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); + DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId(); + + if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) { + ObjectNode newDetails = JacksonUtil.newObjectNode(); + if (StringUtils.isNotEmpty(alarmDetailsStr)) { + for (Map.Entry entry : arguments.entrySet()) { + String key = entry.getKey(); + ArgumentEntry value = entry.getValue(); + alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", key), String.valueOf(value.getValue())); + } + newDetails.put("data", alarmDetailsStr); + } + if (dashboardId != null) { + newDetails.put("dashboardId", dashboardId.getId().toString()); + } + alarmDetails = newDetails; + } else if (currentAlarm != null) { + alarmDetails = currentAlarm.getDetails(); + } else { + alarmDetails = JacksonUtil.newObjectNode(); + } + + return alarmDetails; + } + + @SneakyThrows + public boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + Object result = ctx.evaluateTbelExpression(tbelExpression.getExpression(), this).get(); + if (result instanceof Boolean booleanResult) { + return booleanResult; + } else { + throw new IllegalStateException("Condition expression returned non-boolean value: '" + result + "'"); + } + } else { + SimpleAlarmConditionExpression simpleExpression = (SimpleAlarmConditionExpression) expression; + ComplexOperation operation = simpleExpression.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> simpleExpression.getFilters().stream() + .allMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + case OR -> simpleExpression.getFilters().stream() + .anyMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + }; + } + } + + private boolean eval(SingleValueArgumentEntry argument, AlarmConditionFilter filter) { + ComplexOperation operation = filter.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> filter.getPredicates().stream() + .allMatch(predicate -> eval(argument, predicate)); + case OR -> filter.getPredicates().stream() + .anyMatch(predicate -> eval(argument, predicate)); + }; + } + + private boolean eval(SingleValueArgumentEntry argument, KeyFilterPredicate predicate) { + return switch (predicate.getType()) { + case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); + case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); + case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); + case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); + }; + } + + private boolean evalComplexPredicate(SingleValueArgumentEntry argument, ComplexFilterPredicate complexPredicate) { + return switch (complexPredicate.getOperation()) { + case OR -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (eval(argument, predicate)) { + yield true; + } + } + yield false; + } + case AND -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (!eval(argument, predicate)) { + yield false; + } + } + yield true; + } + }; + } + + private boolean evalBooleanPredicate(SingleValueArgumentEntry argument, BooleanFilterPredicate predicate) { + Boolean value = KvUtil.getBoolValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Boolean predicateValue = resolveValue(predicate.getValue(), KvUtil::getBoolValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + }; + } + + private boolean evalNumPredicate(SingleValueArgumentEntry argument, NumericFilterPredicate predicate) { + Double value = KvUtil.getDoubleValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Double predicateValue = resolveValue(predicate.getValue(), KvUtil::getDoubleValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case NOT_EQUAL -> !value.equals(predicateValue); + case EQUAL -> value.equals(predicateValue); + case GREATER -> value > predicateValue; + case GREATER_OR_EQUAL -> value >= predicateValue; + case LESS -> value < predicateValue; + case LESS_OR_EQUAL -> value <= predicateValue; + }; + } + + private boolean evalStrPredicate(SingleValueArgumentEntry argument, StringFilterPredicate predicate) { + String value = KvUtil.getStringValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + String predicateValue = resolveValue(predicate.getValue(), KvUtil::getStringValue); + if (predicateValue == null) { + return false; + } + if (predicate.isIgnoreCase()) { + value = value.toLowerCase(); + predicateValue = predicateValue.toLowerCase(); + } + return switch (predicate.getOperation()) { + case CONTAINS -> value.contains(predicateValue); + case EQUAL -> value.equals(predicateValue); + case STARTS_WITH -> value.startsWith(predicateValue); + case ENDS_WITH -> value.endsWith(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + case NOT_CONTAINS -> !value.contains(predicateValue); + case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + }; + } + + protected T resolveValue(AlarmConditionValue conditionValue, Function mapper) { + T value = conditionValue.getStaticValue(); + if (value == null) { + String argument = conditionValue.getDynamicValueArgument(); + SingleValueArgumentEntry entry = getArgument(argument); + value = mapper.apply(entry.getKvEntryValue()); + if (value == null) { + throw new IllegalArgumentException("No proper value found for argument " + argument); + } + } + return value; + } + + protected SingleValueArgumentEntry getArgument(String key) { + SingleValueArgumentEntry entry = (SingleValueArgumentEntry) arguments.get(key); + if (entry == null) { + throw new IllegalArgumentException("Argument '" + key + "' is missing"); + } + return entry; + } + + private AlarmCalculatedFieldConfiguration getConfiguration(CalculatedFieldCtx ctx) { + return (AlarmCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Only single value arguments supported"); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java new file mode 100644 index 0000000000..424a977c75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.alarm; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class AlarmEvalResult { + + public static final AlarmEvalResult TRUE = new AlarmEvalResult(Status.TRUE); + public static final AlarmEvalResult FALSE = new AlarmEvalResult(Status.FALSE); + public static final AlarmEvalResult NOT_YET_TRUE = new AlarmEvalResult(Status.NOT_YET_TRUE); + + private final Status status; + private final long leftDuration; + private final long leftEvents; + + public AlarmEvalResult(Status status) { + this(status, 0, 0); + } + + public static AlarmEvalResult notYetTrue(long leftEvents, long leftDuration) { + return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); + } + + public enum Status { + FALSE, NOT_YET_TRUE, TRUE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java new file mode 100644 index 0000000000..8612607dfb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -0,0 +1,344 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.alarm; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmScheduleType; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeScheduleItem; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + +@Data +@Slf4j +public class AlarmRuleState { + + private final AlarmSeverity severity; + private AlarmRule alarmRule; + private AlarmCalculatedFieldState state; + + private AlarmCondition condition; + + private long eventCount; + private long firstEventTs; // when duration condition started + private long lastEventTs; + private transient long duration; + private ScheduledFuture durationCheckFuture; + private Boolean active; + + public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, AlarmCalculatedFieldState state) { + this.severity = severity; + if (alarmRule != null) { + setAlarmRule(alarmRule); + } + this.state = state; + } + + public AlarmEvalResult eval(boolean newEvent, CalculatedFieldCtx ctx) { // on event or config change + long ts = newEvent ? state.getLatestTimestamp() : System.currentTimeMillis(); + active = isActive(ts); + if (!active) { + return AlarmEvalResult.FALSE; + } + return doEval(newEvent, ctx); + } + + public AlarmEvalResult reeval(long ts, CalculatedFieldCtx ctx) { // on scheduled duration check or periodic re-eval for rules with schedule + boolean active = isActive(ts); + switch (condition.getType()) { + case SIMPLE, REPEATING -> { + if (this.active == null || active != this.active) { + this.active = active; + if (active) { + return doEval(false, ctx); + } + } + if (active) { + return AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } + } + case DURATION -> { + if (!active) { + return AlarmEvalResult.FALSE; + } + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0 && lastEventTs > 0 && ts > lastEventTs) { + duration = ts - firstEventTs; + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } + } + } + return AlarmEvalResult.FALSE; + } + + public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { + return switch (condition.getType()) { + case SIMPLE -> evalSimple(ctx); + case DURATION -> evalDuration(ctx); + case REPEATING -> evalRepeating(newEvent, ctx); + }; + } + + private AlarmEvalResult evalSimple(CalculatedFieldCtx ctx) { + return eval(condition.getExpression(), ctx) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + } + + private AlarmEvalResult evalRepeating(boolean newEvent, CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + if (newEvent) { + eventCount++; + } + long requiredRepeats = getIntValue(((RepeatingAlarmCondition) condition).getCount()); + if (requiredRepeats > 0) { + long leftRepeats = requiredRepeats - eventCount; + return leftRepeats <= 0 ? AlarmEvalResult.TRUE : AlarmEvalResult.notYetTrue(leftRepeats, 0); + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private AlarmEvalResult evalDuration(CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + long eventTs = state.getLatestTimestamp(); + if (lastEventTs > 0) { + if (eventTs > lastEventTs) { + if (firstEventTs == 0) { + firstEventTs = lastEventTs; + } + lastEventTs = eventTs; + } + } else { + firstEventTs = eventTs; + lastEventTs = eventTs; + } + duration = lastEventTs - firstEventTs; + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0) { + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private boolean isActive(long eventTs) { + if (condition.getSchedule() == null) { + return true; + } + AlarmSchedule schedule = state.resolveValue(condition.getSchedule(), entry -> Optional.ofNullable(KvUtil.getStringValue(entry)) + .map(this::parseSchedule).orElse(null)); + boolean active = switch (schedule.getType()) { + case ANY_TIME -> true; + case SPECIFIC_TIME -> isActiveSpecific((SpecificTimeSchedule) schedule, eventTs); + case CUSTOM -> isActiveCustom((CustomTimeSchedule) schedule, eventTs); + }; + log.trace("Alarm rule active = {} for schedule {}", active, schedule); + return active; + } + + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + if (schedule.getDaysOfWeek().size() != 7) { + int dayOfWeek = zdt.getDayOfWeek().getValue(); + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) { + return false; + } + } + long endsOn = schedule.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn); + } + + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue(); + for (CustomTimeScheduleItem item : schedule.getItems()) { + if (item.getDayOfWeek() == dayOfWeek) { + if (item.isEnabled()) { + long endsOn = item.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn); + } else { + return false; + } + } + } + return false; + } + + private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) { + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + if (startsOn <= endsOn) { + return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay; + } else { + return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn); + } + } + + public void clear() { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + + private void clearRepeatingConditionState() { + eventCount = 0L; + } + + private void clearDurationConditionState() { + firstEventTs = 0L; + lastEventTs = 0L; + duration = 0L; + if (durationCheckFuture != null) { + durationCheckFuture.cancel(true); + durationCheckFuture = null; + } + } + + public boolean isEmpty() { + return eventCount == 0L && firstEventTs == 0L && lastEventTs == 0L && durationCheckFuture == null; + } + + private AlarmSchedule parseSchedule(String str) { + ObjectNode json = (ObjectNode) JacksonUtil.toJsonNode(str); + if (json.isEmpty()) { + return new AnyTimeSchedule(); // only if valid json, fail otherwise + } + + if (!json.hasNonNull("type")) { + // deducting the schedule type + AlarmScheduleType type; + if (json.hasNonNull("daysOfWeek")) { + type = AlarmScheduleType.SPECIFIC_TIME; + } else if (json.hasNonNull("items")) { + type = AlarmScheduleType.CUSTOM; + } else { + throw new IllegalArgumentException("Failed to parse alarm schedule from '" + str + "'"); + } + json.put("type", type.name()); + } + + return JacksonUtil.treeToValue(json, AlarmSchedule.class); + } + + private Integer getIntValue(AlarmConditionValue value) { + return state.resolveValue(value, entry -> Optional.ofNullable(KvUtil.getLongValue(entry)).map(Long::intValue).orElse(null)); + } + + private long getRequiredDurationInMs() { + DurationAlarmCondition durationCondition = (DurationAlarmCondition) condition; + return durationCondition.getUnit().toMillis(state.resolveValue(durationCondition.getValue(), KvUtil::getLongValue)); + } + + private boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + return state.eval(expression, ctx); + } + + public void setAlarmRule(AlarmRule alarmRule) { + this.alarmRule = alarmRule; + this.condition = alarmRule.getCondition(); + + // clearing state for other condition types (possibly left from a previous condition type) + switch (condition.getType()) { + case SIMPLE -> { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + case REPEATING -> { + clearDurationConditionState(); + } + case DURATION -> { + clearRepeatingConditionState(); + } + } + } + + public StateInfo getStateInfo() { + if (condition.getType() == AlarmConditionType.REPEATING) { + return new StateInfo(eventCount, null); + } else if (condition.getType() == AlarmConditionType.DURATION) { + return new StateInfo(null, duration); + } else { + return StateInfo.EMPTY; + } + } + + @Override + public String toString() { + return "AlarmRuleState{" + + "severity=" + severity + + ", condition=" + condition + + ", eventCount=" + eventCount + + ", firstEventTs=" + firstEventTs + + ", lastEventTs=" + lastEventTs + + ", duration=" + duration + + ", durationCheckFuture=" + durationCheckFuture + + '}'; + } + + public record StateInfo(Long eventCount, Long duration) { + static final StateInfo EMPTY = new StateInfo(null, null); + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index 53e5c19e72..bcc4d3ffcd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -18,7 +18,7 @@ package org.thingsboard.server.service.cf.ctx.state.geofencing; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; +import org.thingsboard.script.api.tbel.TbelCfGeofencingArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.util.ProtoUtils; @@ -83,7 +83,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { - return new TbelCfTsGeofencingArg(zoneStates); + return new TbelCfGeofencingArg(zoneStates); } private Map toZones(Map entityIdKvEntryMap) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index 398de0c20b..51110df2bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -20,9 +20,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; @@ -51,16 +52,16 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Ent import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; -@Data +@Getter +@Setter @Slf4j -@NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { private long lastDynamicArgumentsRefreshTs = -1; - public GeofencingCalculatedFieldState(List requiredArguments) { - super(requiredArguments); + public GeofencingCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override @@ -87,7 +88,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); @@ -128,13 +129,27 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { }); OutputType outputType = ctx.getOutput().getType(); - var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode)); + var result = TelemetryCalculatedFieldResult.builder() + .type(outputType) + .scope(ctx.getOutput().getScope()) + .result(toResultNode(outputType, valuesNode)) + .build(); if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor()); } + @Override + public void reset() { + super.reset(); + lastDynamicArgumentsRefreshTs = -1; + } + + public void updateLastDynamicArgumentsRefreshTs() { + lastDynamicArgumentsRefreshTs = System.currentTimeMillis(); + } + private Map getGeofencingArguments() { return arguments.entrySet() .stream() @@ -157,13 +172,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) { - if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) { - return valuesNode; - } - ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", latestTimestamp); - resultNode.set("values", valuesNode); - return resultNode; + return toSimpleResult(outputType == OutputType.TIME_SERIES, valuesNode); } private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java new file mode 100644 index 0000000000..81009da5e5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.propagation; + +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class PropagationArgumentEntry implements ArgumentEntry { + + private List propagationEntityIds; + + private boolean forceResetPrevious; + + public PropagationArgumentEntry(List propagationEntityIds) { + this.propagationEntityIds = new ArrayList<>(propagationEntityIds); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.PROPAGATION; + } + + @Override + public Object getValue() { + return propagationEntityIds; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); + } + if (propagationArgumentEntry.isEmpty()) { + propagationEntityIds.clear(); + } else { + propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); + } + return true; + } + + @Override + public boolean isEmpty() { + return CollectionsUtil.isEmpty(propagationEntityIds); + } + + @Override + public TbelCfArg toTbelCfArg() { + return new TbelCfPropagationArg(propagationEntityIds); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java new file mode 100644 index 0000000000..4f589572e8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state.propagation; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.ArrayList; +import java.util.Map; + +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { + + public PropagationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.requiredArguments = new ArrayList<>(ctx.getArgNames()); + requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); + this.readinessStatus = checkReadiness(requiredArguments, arguments); + if (ctx.isApplyExpressionForResolvedArguments()) { + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry) || propagationArgumentEntry.isEmpty()) { + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); + } + if (ctx.isApplyExpressionForResolvedArguments()) { + return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> + PropagationCalculatedFieldResult.builder() + .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .result((TelemetryCalculatedFieldResult) telemetryCfResult) + .build(), + MoreExecutors.directExecutor()); + } + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() + .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .result(toTelemetryResult(ctx)) + .build()); + } + + private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) { + Output output = ctx.getOutput(); + TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = + TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()); + ObjectNode valuesNode = JacksonUtil.newObjectNode(); + arguments.forEach((outputKey, argumentEntry) -> { + if (argumentEntry instanceof PropagationArgumentEntry) { + return; + } + if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { + JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + return; + } + throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + + "Only Latest telemetry or Attribute arguments supported for 'Arguments Only' propagation mode!"); + }); + ObjectNode result = toSimpleResult(output.getType() == OutputType.TIME_SERIES, valuesNode); + telemetryCfBuilder.result(result); + return telemetryCfBuilder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index 8a111e4d9d..d942dc2277 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -54,16 +54,18 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - executorService.submit(() -> { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - try { - switch (event.getActionType()) { - case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); - } - } catch (Exception e) { - log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + switch (event.getActionType()) { + case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> { + executorService.submit(() -> { + log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); + try { + relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + } catch (Exception e) { + log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + } + }); } - }); + } } @TransactionalEventListener( diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index e8c2f65975..d6fd2f6968 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -77,7 +77,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_CLEAR_RPC_MESSAGE: Alarm alarmToClear = edgeCtx.getAlarmService().findAlarmById(tenantId, alarmId); if (alarmToClear != null) { - edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails()); + edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails(), true); } break; case ENTITY_DELETED_RPC_MESSAGE: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java index 4ef6ec7ba2..c3f9300e24 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -53,7 +53,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { } String calculatedFieldName = calculatedField.getName(); - CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndTypeAndName(calculatedField.getEntityId(), calculatedField.getType(), calculatedFieldName); if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 03ab77ac09..9a6a96155f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -22,8 +22,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -31,9 +33,11 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceId; @@ -44,6 +48,7 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -53,13 +58,18 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.service.cf.CalculatedFieldCache; import java.util.Set; @@ -72,6 +82,7 @@ public class EntityStateSourcingListener { private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; private final JobManager jobManager; + private final CalculatedFieldCache calculatedFieldCache; @PostConstruct public void init() { @@ -140,6 +151,9 @@ public class EntityStateSourcingListener { case JOB -> { onJobUpdate((Job) event.getEntity()); } + case CUSTOMER -> { + tbClusterService.onCustomerUpdated((Customer) event.getEntity(), (Customer) event.getOldEntity()); + } default -> { } } @@ -153,7 +167,7 @@ public class EntityStateSourcingListener { return; } EntityType entityType = entityId.getEntityType(); - if (!tenantId.isSysTenantId() && entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { + if (entityType != EntityType.TENANT && !tenantExists(tenantId)) { log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); return; } @@ -216,18 +230,57 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) && - EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && event.getEntity() instanceof DeviceCredentials) { - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), - (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); - } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { - Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); - if (tenant != null) { - tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + TenantId tenantId = event.getTenantId(); + log.trace("[{}] ActionEntityEvent called: {}", tenantId, event); + switch (event.getActionType()) { + case CREDENTIALS_UPDATED -> { + if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && + event.getEntity() instanceof DeviceCredentials deviceCredentials) { + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, + (DeviceId) event.getEntityId(), deviceCredentials), null); + } + } + case ASSIGNED_TO_TENANT -> { + if (event.getEntity() instanceof Device device) { + Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); + if (tenant != null) { + tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + } + pushAssignedFromNotification(tenant, tenantId, device); + } + } + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + if (event.getActionType() == ActionType.ALARM_DELETE && !tenantExists(tenantId)) { + return; + } + Alarm alarm = (Alarm) event.getEntity(); + if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) { + ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder() + .setEventMsg(toProto(event)) + .build(); + tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) {} + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to push alarm event to CF queue: {}", tenantId, event, t); + } + }); + } + } + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(RelationActionEvent relationEvent) { + EntityRelation relation = relationEvent.getRelation(); + if (CalculatedField.isSupportedRefEntity(relation.getFrom()) && CalculatedField.isSupportedRefEntity(relation.getTo())) { + if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) { + tbClusterService.onRelationUpdated(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); + } else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) { + tbClusterService.onRelationDeleted(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); } - pushAssignedFromNotification(tenant, event.getTenantId(), device); } } @@ -338,6 +391,10 @@ public class EntityStateSourcingListener { } } + private boolean tenantExists(TenantId tenantId) { + return tenantId.isSysTenantId() || tenantService.tenantExists(tenantId); + } + private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); @@ -345,4 +402,13 @@ public class EntityStateSourcingListener { return metaData; } + private EntityActionEventProto toProto(ActionEntityEvent event) { + return EntityActionEventProto.newBuilder() + .setTenantId(ProtoUtils.toProto(event.getTenantId())) + .setEntityId(ProtoUtils.toProto(event.getEntityId())) + .setAction(event.getActionType().name()) + .setEntity(event.getEntity() != null ? JacksonUtil.toString(event.getEntity()) : "") + .build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index 4dfaec91cf..63f8a9bf2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,7 +34,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Optional; +import java.util.Set; @TbCoreComponent @Service @@ -52,7 +53,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp CalculatedField existingCf = calculatedFieldService.findById(tenantId, calculatedField.getId()); checkForEntityChange(existingCf, calculatedField); } - checkEntityExistence(tenantId, calculatedField.getEntityId()); + checkEntity(tenantId, calculatedField.getEntityId(), calculatedField.getType()); CalculatedField savedCalculatedField = checkNotNull(calculatedFieldService.save(calculatedField)); logEntityActionService.logEntityAction(tenantId, savedCalculatedField.getId(), savedCalculatedField, actionType, user); return savedCalculatedField; @@ -68,10 +69,9 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } @Override - public PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink) { - TenantId tenantId = user.getTenantId(); - checkEntityExistence(tenantId, entityId); - return calculatedFieldService.findAllCalculatedFieldsByEntityId(tenantId, entityId, pageLink); + public PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { + checkEntity(tenantId, entityId, type); + return calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId, type, pageLink); } @Override @@ -95,11 +95,15 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } } - private void checkEntityExistence(TenantId tenantId, EntityId entityId) { - switch (entityId.getEntityType()) { - case ASSET, DEVICE, ASSET_PROFILE, DEVICE_PROFILE -> Optional.ofNullable(entityService.fetchEntity(tenantId, entityId)) - .orElseThrow(() -> new IllegalArgumentException(entityId.getEntityType().getNormalName() + " with id [" + entityId.getId() + "] does not exist.")); - default -> throw new IllegalArgumentException("Entity type '" + entityId.getEntityType() + "' does not support calculated fields."); + private void checkEntity(TenantId tenantId, EntityId entityId, CalculatedFieldType type) { + EntityType entityType = entityId.getEntityType(); + Set supportedTypes = CalculatedField.SUPPORTED_ENTITIES.get(entityType); + if (supportedTypes == null || supportedTypes.isEmpty()) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support calculated fields"); + } else if (type != null && !supportedTypes.contains(type)) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support '" + type + "' calculated fields"); + } else if (entityService.fetchEntity(tenantId, entityId).isEmpty()) { + throw new IllegalArgumentException(entityType.getNormalName() + " with id [" + entityId.getId() + "] does not exist."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 1e04a14a08..20705aaaff 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -16,9 +16,11 @@ package org.thingsboard.server.service.entitiy.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.service.security.model.SecurityUser; @@ -29,7 +31,7 @@ public interface TbCalculatedFieldService { CalculatedField findById(CalculatedFieldId calculatedFieldId, SecurityUser user); - PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink); + PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void delete(CalculatedField calculatedField, SecurityUser user); 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 d580175aa0..a55832c823 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 @@ -49,17 +49,25 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.device.profile.AlarmCondition; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -74,12 +82,7 @@ import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; @@ -94,6 +97,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -117,7 +121,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.TreeMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -155,6 +159,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final MobileAppDao mobileAppDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; + private final CalculatedFieldService calculatedFieldService; @Autowired private BCryptPasswordEncoder passwordEncoder; @@ -306,8 +311,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { if (invalidSignKey) { log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " + - "You can change the JWT Signing Key using the Web UI: " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); + "You can change the JWT Signing Key using the Web UI: " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); jwtSettings.setTokenSigningKey(generateRandomKey()); jwtSettingsService.saveJwtSettings(jwtSettings); @@ -319,9 +324,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) .forEach(mobileApp -> { log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + - "A new Application Secret has been added automatically for Mobile Application [{}]. " + - "You can change the Application Secret using the Web UI: " + - "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); + "A new Application Secret has been added automatically for Mobile Application [{}]. " + + "You can change the Application Secret using the Web UI: " + + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); mobileApp.setAppSecret(generateRandomKey()); mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp); }); @@ -372,11 +377,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createDevice(demoTenant.getId(), customerB.getId(), defaultDeviceProfile.getId(), "Test Device B1", "B1_TEST_TOKEN", null); createDevice(demoTenant.getId(), customerC.getId(), defaultDeviceProfile.getId(), "Test Device C1", "C1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + - "applications that upload data from DHT11 temperature and humidity sensor"); + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", + "Demo device that is used in sample applications that upload data from DHT11 temperature and humidity sensor"); - createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + - "Raspberry Pi GPIO control sample application"); + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", + "Demo device that is used in Raspberry Pi GPIO control sample application"); DeviceProfile thermostatDeviceProfile = new DeviceProfile(); thermostatDeviceProfile.setTenantId(demoTenant.getId()); @@ -398,110 +403,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { deviceProfileData.setProvisionConfiguration(provisionConfiguration); thermostatDeviceProfile.setProfileData(deviceProfileData); - DeviceProfileAlarm highTemperature = new DeviceProfileAlarm(); - highTemperature.setId("highTemperatureAlarmID"); - highTemperature.setAlarmType("High Temperature"); - AlarmRule temperatureRule = new AlarmRule(); - AlarmCondition temperatureCondition = new AlarmCondition(); - temperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter temperatureAlarmFlagAttributeFilter = new AlarmConditionFilter(); - temperatureAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "temperatureAlarmFlag")); - temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate); - - AlarmConditionFilter temperatureTimeseriesFilter = new AlarmConditionFilter(); - temperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - FilterPredicateValue temperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue); - temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate); - temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter)); - temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - temperatureRule.setCondition(temperatureCondition); - highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule))); - - AlarmRule clearTemperatureRule = new AlarmRule(); - AlarmCondition clearTemperatureCondition = new AlarmCondition(); - clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearTemperatureTimeseriesFilter = new AlarmConditionFilter(); - clearTemperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); - FilterPredicateValue clearTemperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - - clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue); - clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate); - clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter)); - clearTemperatureRule.setCondition(clearTemperatureCondition); - clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - highTemperature.setClearRule(clearTemperatureRule); - - DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm(); - lowHumidity.setId("lowHumidityAlarmID"); - lowHumidity.setAlarmType("Low Humidity"); - AlarmRule humidityRule = new AlarmRule(); - AlarmCondition humidityCondition = new AlarmCondition(); - humidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); - humidityAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "humidityAlarmFlag")); - humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate); - - AlarmConditionFilter humidityTimeseriesFilter = new AlarmConditionFilter(); - humidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); - FilterPredicateValue humidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue); - humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate); - humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter)); - - humidityRule.setCondition(humidityCondition); - humidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule))); - - AlarmRule clearHumidityRule = new AlarmRule(); - AlarmCondition clearHumidityCondition = new AlarmCondition(); - clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearHumidityTimeseriesFilter = new AlarmConditionFilter(); - clearHumidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); - FilterPredicateValue clearHumidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - - clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue); - clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate); - clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter)); - clearHumidityRule.setCondition(clearHumidityCondition); - clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setClearRule(clearHumidityRule); - - deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity)); - DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile); + createAlarmRules(demoTenant.getId(), savedThermostatDeviceProfile.getId()); DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); @@ -526,6 +429,136 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.createDefaultTenantDashboards(demoTenant.getId(), null); } + private void createAlarmRules(TenantId tenantId, DeviceProfileId deviceProfileId) { + CalculatedField highTemperature = new CalculatedField(); + highTemperature.setName("High Temperature"); + highTemperature.setType(CalculatedFieldType.ALARM); + highTemperature.setTenantId(tenantId); + highTemperature.setEntityId(deviceProfileId); + highTemperature.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration highTemperatureConfig = new AlarmCalculatedFieldConfiguration(); + highTemperature.setConfiguration(highTemperatureConfig); + + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setDefaultValue("25"); + Argument temperatureAlarmFlagArgument = new Argument(); + temperatureAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + highTemperatureConfig.setArguments(Map.of( + "temperature", temperatureArgument, + "temperatureAlarmThreshold", temperatureThresholdArgument, + "temperatureAlarmFlag", temperatureAlarmFlagArgument + )); + + AlarmRule temperatureRule = new AlarmRule(); + SimpleAlarmCondition temperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter temperatureAlarmFlagFilter = new AlarmConditionFilter(); + temperatureAlarmFlagFilter.setArgument("temperatureAlarmFlag"); + temperatureAlarmFlagFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); + temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + temperatureAlarmFlagAttributePredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + temperatureAlarmFlagFilter.setPredicates(List.of(temperatureAlarmFlagAttributePredicate)); + + AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); + temperatureFilter.setArgument("temperature"); + temperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate temperatureFilterPredicate = new NumericFilterPredicate(); + temperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + temperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + temperatureFilter.setPredicates(List.of(temperatureFilterPredicate)); + temperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(temperatureAlarmFlagFilter, temperatureFilter), ComplexOperation.AND)); + temperatureRule.setCondition(temperatureCondition); + temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setCreateRules(Map.of( + AlarmSeverity.MAJOR, temperatureRule + )); + + AlarmRule clearTemperatureRule = new AlarmRule(); + SimpleAlarmCondition clearTemperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearTemperatureFilter = new AlarmConditionFilter(); + clearTemperatureFilter.setArgument("temperature"); + clearTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearTemperatureFilterPredicate = new NumericFilterPredicate(); + clearTemperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); + clearTemperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + clearTemperatureFilter.setPredicates(List.of(clearTemperatureFilterPredicate)); + clearTemperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearTemperatureFilter), ComplexOperation.AND)); + clearTemperatureRule.setCondition(clearTemperatureCondition); + clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setClearRule(clearTemperatureRule); + + calculatedFieldService.save(highTemperature); + + CalculatedField lowHumidity = new CalculatedField(); + lowHumidity.setName("Low Humidity"); + lowHumidity.setType(CalculatedFieldType.ALARM); + lowHumidity.setTenantId(tenantId); + lowHumidity.setEntityId(deviceProfileId); + lowHumidity.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration lowHumidityConfig = new AlarmCalculatedFieldConfiguration(); + lowHumidity.setConfiguration(lowHumidityConfig); + + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + Argument humidityThresholdArgument = new Argument(); + humidityThresholdArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityThresholdArgument.setDefaultValue("60"); + Argument humidityAlarmFlagArgument = new Argument(); + humidityAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + lowHumidityConfig.setArguments(Map.of( + "humidity", humidityArgument, + "humidityAlarmThreshold", humidityThresholdArgument, + "humidityAlarmFlag", humidityAlarmFlagArgument + )); + + AlarmRule humidityRule = new AlarmRule(); + SimpleAlarmCondition humidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); + humidityAlarmFlagAttributeFilter.setArgument("humidityAlarmFlag"); + humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate humidityAlarmFlagPredicate = new BooleanFilterPredicate(); + humidityAlarmFlagPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + humidityAlarmFlagPredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + humidityAlarmFlagAttributeFilter.setPredicates(List.of(humidityAlarmFlagPredicate)); + + AlarmConditionFilter humidityFilter = new AlarmConditionFilter(); + humidityFilter.setArgument("humidity"); + humidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humidityFilterPredicate = new NumericFilterPredicate(); + humidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + humidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + humidityFilter.setPredicates(List.of(humidityFilterPredicate)); + humidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(humidityAlarmFlagAttributeFilter, humidityFilter), ComplexOperation.AND)); + humidityRule.setCondition(humidityCondition); + humidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setCreateRules(Map.of( + AlarmSeverity.MINOR, humidityRule + )); + + AlarmRule clearHumidityRule = new AlarmRule(); + SimpleAlarmCondition clearHumidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearHumidityFilter = new AlarmConditionFilter(); + clearHumidityFilter.setArgument("humidity"); + clearHumidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearHumidityFilterPredicate = new NumericFilterPredicate(); + clearHumidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); + clearHumidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + clearHumidityFilter.setPredicates(List.of(clearHumidityFilterPredicate)); + clearHumidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearHumidityFilter), ComplexOperation.AND)); + clearHumidityRule.setCondition(clearHumidityCondition); + clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setClearRule(clearHumidityRule); + + calculatedFieldService.save(lowHumidity); + } + @Override public void loadSystemWidgets() throws Exception { installScripts.loadSystemWidgets(); @@ -609,6 +642,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public void onFailure(Throwable t) { log.warn("[{}] Failed to update attribute [{}] with value [{}]", deviceId, key, value, t); } + } private void addTsCallback(ListenableFuture saveFuture, final FutureCallback callback) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java index 4422d952a5..8b7218a981 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java @@ -19,14 +19,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ProjectInfo { - private final BuildProperties buildProperties; + private final Optional buildProperties; public String getProjectVersion() { - return buildProperties.getVersion().replaceAll("[^\\d.]", ""); + return buildProperties.orElseThrow(() -> new IllegalStateException("Build properties are missing. Please rebuild the project with maven")) + .getVersion().replaceAll("[^\\d.]", ""); } public String getProductType() { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index 691be2e894..a1df9b3e58 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; @@ -62,8 +63,9 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc originatorName = ((AlarmInfo) alarm).getOriginatorName(); originatorLabel = ((AlarmInfo) alarm).getOriginatorLabel(); } else { - originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse(""); - originatorLabel = entityService.fetchEntityLabel(trigger.getTenantId(), alarm.getOriginator()).orElse(""); + var infoOpt = entityService.fetchNameLabelAndCustomerDetails(trigger.getTenantId(), alarm.getOriginator()); + originatorName = infoOpt.map(NameLabelAndCustomerDetails::getName).orElse(null); + originatorLabel = infoOpt.map(NameLabelAndCustomerDetails::getLabel).orElse(null); } return AlarmCommentNotificationInfo.builder() .comment(trigger.getComment().getComment().get("text").asText()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index af80d7f2ab..5d4be59b97 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -118,15 +118,15 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor toInfoTemplateMap(JsonNode details) { + private Map toDetailsTemplateMap(JsonNode details) { Map infoMap = JacksonUtil.toFlatMap(details); Map result = new HashMap<>(); for (Map.Entry entry : infoMap.entrySet()) { - String key = "info." + entry.getKey(); + String key = "details." + entry.getKey(); result.put(key, entry.getValue()); } return result; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index acb36449e8..0449d116c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActionEventMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.DataConstants; @@ -160,12 +161,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa try { ToCalculatedFieldMsg toCfMsg = msg.getValue(); pendingMsgHolder.setMsg(toCfMsg); - if (toCfMsg.hasTelemetryMsg()) { - log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); - forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); - } else if (toCfMsg.hasLinkedTelemetryMsg()) { - forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } + processMsg(toCfMsg, id, callback); } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); callback.onFailure(e); @@ -183,6 +179,17 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa consumer.commit(); } + private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) { + if (toCfMsg.hasTelemetryMsg()) { + log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); + forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); + } else if (toCfMsg.hasLinkedTelemetryMsg()) { + forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); + } else if (toCfMsg.hasEventMsg()) { + actorContext.tell(CalculatedFieldEntityActionEventMsg.fromProto(toCfMsg.getEventMsg(), callback)); + } + } + @Override protected ServiceType getServiceType() { return ServiceType.TB_RULE_ENGINE; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 86d00c77ca..b54a7fb83a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -55,6 +56,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -468,6 +470,17 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED); } + @Override + public void onCustomerUpdated(Customer customer, Customer oldCustomer) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(customer.getTenantId()) + .entityId(customer.getId()) + .event(oldCustomer == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED) + .ownerChanged(false) // for compatibility with PE + .build(); + broadcast(msg); + } + private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName(); log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName); @@ -597,7 +610,8 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.JOB, - EntityType.TB_RESOURCE) + EntityType.TB_RESOURCE, + EntityType.CUSTOMER) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { @@ -673,6 +687,7 @@ public class DefaultTbClusterService implements TbClusterService { } msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getDeviceProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -693,6 +708,7 @@ public class DefaultTbClusterService implements TbClusterService { } else { msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getAssetProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -708,6 +724,28 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } + @Override + public void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_UPDATED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + + @Override + public void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_DELETED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + @Override public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId originatorEdgeId) { if (!edgesEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index 40ef9bfeca..806f8877ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -87,8 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, - Object consumerKey, + ConsumerKey consumerKey, Queue queue) throws Exception { TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index b7b027d785..5d634c4178 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -154,12 +155,20 @@ public class DefaultEntityExportService calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId); calculatedFields.forEach(calculatedField -> { calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId)); - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { - configuration.getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); - } - }); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(getExternalIdOrElseInternal(ctx, zoneGroupConfiguration.getRefEntityId())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + } + }); + } } }); return calculatedFields; diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 768adf98ff..bdea24bb2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -193,7 +193,7 @@ public abstract class AbstractBulkImportService kvsEntry.getValue().entrySet().size() > 0) + .filter(kvsEntry -> !kvsEntry.getValue().entrySet().isEmpty()) .forEach(kvsEntry -> { BulkImportColumnType kvType = kvsEntry.getKey(); if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 2b606a2fe8..c95fffc205 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -322,12 +323,20 @@ public abstract class BaseEntityImportService { calculatedField.setTenantId(ctx.getTenantId()); calculatedField.setEntityId(savedEntity.getId()); - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration) { - configuration.getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); - } - }); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(idProvider.getInternalId(zoneGroupConfiguration.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } } }).toList(); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 8c4a375fae..b68f604460 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -102,7 +102,12 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { - return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + return clearAlarm(tenantId, alarmId, clearTs, details, true); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details, pushEvent)); } @Override diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index 934eadd98f..0c0d401688 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -21,6 +21,9 @@ import com.google.common.util.concurrent.MoreExecutors; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; @@ -28,10 +31,13 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.Optional; @@ -64,11 +70,19 @@ public class CalculatedFieldArgumentUtils { return new StringDataEntry(key, defaultValue); } - public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { + public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { + KvEntry kvEntry = createDefaultKvEntry(argument); + return new BaseAttributeKvEntry(kvEntry, ts, 0L); + } + + public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx, EntityId entityId) { return switch (ctx.getCfType()) { - case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); - case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); - case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames()); + case SIMPLE -> new SimpleCalculatedFieldState(entityId); + case SCRIPT -> new ScriptCalculatedFieldState(entityId); + case GEOFENCING -> new GeofencingCalculatedFieldState(entityId); + case ALARM -> new AlarmCalculatedFieldState(entityId); + case PROPAGATION -> new PropagationCalculatedFieldState(entityId); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(entityId); }; } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 4e93c8233e..121febea7d 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -17,6 +17,7 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -26,6 +27,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmRuleStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmStateProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; @@ -38,14 +41,20 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -87,17 +96,51 @@ public class CalculatedFieldUtils { .setType(state.getType().name()); state.getArguments().forEach((argName, argEntry) -> { - if (argEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - builder.addSingleValueArguments(toSingleValueArgumentProto(argName, singleValueArgumentEntry)); - } else if (argEntry instanceof TsRollingArgumentEntry rollingArgumentEntry) { - builder.addRollingValueArguments(toRollingArgumentProto(argName, rollingArgumentEntry)); - } else if (argEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { - builder.addGeofencingArguments(toGeofencingArgumentProto(argName, geofencingArgumentEntry)); + switch (argEntry.getType()) { + case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); + case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); + case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); + case RELATED_ENTITIES -> { + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; + relatedEntitiesArgumentEntry.getEntityInputs() + .forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry))); + } } }); + if (state instanceof AlarmCalculatedFieldState alarmState) { + AlarmStateProto.Builder alarmStateProto = AlarmStateProto.newBuilder(); + alarmState.getCreateRuleStates().forEach((severity, ruleState) -> { + alarmStateProto.addCreateRuleStates(toAlarmRuleStateProto(ruleState)); + }); + if (alarmState.getClearRuleState() != null) { + alarmStateProto.setClearRuleState(toAlarmRuleStateProto(alarmState.getClearRuleState())); + } + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + builder.setLastArgsUpdateTs(aggState.getLastArgsRefreshTs()); + builder.setLastMetricsEvalTs(aggState.getLastMetricsEvalTs()); + } return builder.build(); } + private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { + return AlarmRuleStateProto.newBuilder() + .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) + .setEventCount(ruleState.getEventCount()) + .setFirstEventTs(ruleState.getFirstEventTs()) + .setLastEventTs(ruleState.getLastEventTs()) + .build(); + } + + private static AlarmRuleState fromAlarmRuleStateProto(AlarmRuleStateProto proto, AlarmCalculatedFieldState state) { + AlarmSeverity severity = StringUtils.isNotEmpty(proto.getSeverity()) ? AlarmSeverity.valueOf(proto.getSeverity()) : null; + AlarmRuleState ruleState = new AlarmRuleState(severity, null, state); + ruleState.setEventCount(proto.getEventCount()); + ruleState.setFirstEventTs(proto.getFirstEventTs()); + ruleState.setLastEventTs(proto.getLastEventTs()); + return ruleState; + } + public static SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) { SingleValueArgumentProto.Builder builder = SingleValueArgumentProto.newBuilder() .setArgName(argName); @@ -108,6 +151,10 @@ public class CalculatedFieldUtils { Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion); + if (entry.getEntityId() != null) { + builder.setEntityId(ProtoUtils.toProto(entry.getEntityId())); + } + return builder.build(); } @@ -143,7 +190,7 @@ public class CalculatedFieldUtils { return builder.build(); } - public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { + public static CalculatedFieldState fromProto(CalculatedFieldEntityCtxId id, CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; } @@ -151,22 +198,52 @@ public class CalculatedFieldUtils { CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType()); CalculatedFieldState state = switch (type) { - case SIMPLE -> new SimpleCalculatedFieldState(); - case SCRIPT -> new ScriptCalculatedFieldState(); - case GEOFENCING -> new GeofencingCalculatedFieldState(); + case SIMPLE -> new SimpleCalculatedFieldState(id.entityId()); + case SCRIPT -> new ScriptCalculatedFieldState(id.entityId()); + case GEOFENCING -> new GeofencingCalculatedFieldState(id.entityId()); + case ALARM -> new AlarmCalculatedFieldState(id.entityId()); + case PROPAGATION -> new PropagationCalculatedFieldState(id.entityId()); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(id.entityId()); }; - proto.getSingleValueArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map> arguments = new HashMap<>(); + proto.getSingleValueArgumentsList().forEach(argProto -> { + SingleValueArgumentEntry entry = fromSingleValueArgumentProto(argProto); + arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()).put(entry.getEntityId(), entry); + }); + arguments.forEach((argName, entityInputs) -> { + relatedEntitiesAggState.getArguments().put(argName, new RelatedEntitiesArgumentEntry(entityInputs, false)); + }); + relatedEntitiesAggState.setLastArgsRefreshTs(proto.getLastArgsUpdateTs()); + relatedEntitiesAggState.setLastMetricsEvalTs(proto.getLastMetricsEvalTs()); - if (CalculatedFieldType.SCRIPT.equals(type)) { - proto.getRollingValueArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + return relatedEntitiesAggState; } - if (CalculatedFieldType.GEOFENCING.equals(type)) { - proto.getGeofencingArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + proto.getSingleValueArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); + + switch (type) { + case SCRIPT -> { + proto.getRollingValueArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + } + case GEOFENCING -> { + proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + } + case ALARM -> { + AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; + AlarmStateProto alarmStateProto = proto.getAlarmState(); + for (AlarmRuleStateProto ruleStateProto : alarmStateProto.getCreateRuleStatesList()) { + AlarmRuleState ruleState = fromAlarmRuleStateProto(ruleStateProto, alarmState); + alarmState.getCreateRuleStates().put(ruleState.getSeverity(), ruleState); + } + if (alarmStateProto.hasClearRuleState()) { + alarmState.setClearRuleState(fromAlarmRuleStateProto(alarmStateProto.getClearRuleState(), alarmState)); + } + } } return state; @@ -177,11 +254,14 @@ public class CalculatedFieldUtils { return new SingleValueArgumentEntry(); } TsValueProto tsValueProto = proto.getValue(); - return new SingleValueArgumentEntry( - tsValueProto.getTs(), - (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto), - proto.getVersion() - ); + BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto); + long ts = tsValueProto.getTs(); + long version = proto.getVersion(); + if (proto.hasEntityId()) { + EntityId entityId = ProtoUtils.fromProto(proto.getEntityId()); + return new SingleValueArgumentEntry(entityId, ts, kvEntry, version); + } + return new SingleValueArgumentEntry(ts, kvEntry, version); } public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 61b797515a..889df54848 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -529,6 +529,9 @@ actors: configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" # Time in seconds to receive calculation result. calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" + alarms: + # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default. + reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}" debug: settings: diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java new file mode 100644 index 0000000000..b4174e357b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -0,0 +1,907 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.event.CalculatedFieldDebugEvent; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.event.EventDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +@Slf4j +@DaoSqlTest +@TestPropertySource(properties = { + "actors.alarms.reevaluation_interval=1" +}) +public class AlarmRulesTest extends AbstractControllerTest { + + @MockitoSpyBean + private ActorSystemContext actorSystemContext; + + @Autowired + private EventDao eventDao; + + private Device device; + private DeviceId deviceId; + private EntityId originatorId; + private EventId latestEventId; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + device = createDevice("Device A", "aaa"); + deviceId = device.getId(); + originatorId = deviceId; + } + + @Test + public void testCreateAlarm_severityUpdate_clear() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", null, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + Condition clearRule = new Condition("return temperature <= 25;", null, null); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, clearRule); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":101}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":20}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_simpleConditionExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument("temperature"); + filter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); + AlarmConditionValue thresholdValue = new AlarmConditionValue<>(); + thresholdValue.setStaticValue(100.0); + predicate.setValue(thresholdValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + simpleExpression.setOperation(ComplexOperation.AND); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAlarm_repeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + }); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(10); + }); + } + + @Test + public void testCreateAlarm_dynamicRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument eventsCountArgument = new Argument(); + eventsCountArgument.setRefEntityKey(new ReferencedEntityKey("eventsCount", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + eventsCountArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "eventsCount", eventsCountArgument + ); + + int eventsCount = 5; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, + new AlarmConditionValue<>(null, "eventsCount"), null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}"); + for (int i = 0; i < eventsCount; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(eventsCount); + }); + } + + @Test + public void testCreateAlarm_durationCondition() throws Exception { + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + Map arguments = Map.of( + "powerConsumption", argument + ); + + long createDurationMs = 5000L; + long clearDurationMs = 3000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, createDurationMs) + ); + Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds", + arguments, createRules, clearRule); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + Thread.sleep(createDurationMs - 2000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + + postTelemetry(deviceId, "{\"powerConsumption\":2000}"); + Thread.sleep(clearDurationMs - 2000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_dynamicDurationCondition() throws Exception { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("0"); + + Argument durationArgument = new Argument(); + durationArgument.setRefEntityKey(new ReferencedEntityKey("duration", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + durationArgument.setDefaultValue("-1"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument, + "duration", durationArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(null, "duration"), null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_currentOwnerArgument() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("1000"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + postTelemetry(deviceId, "{\"temperature\":51}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAndClearAlarm_customerAlarmRule_simpleExpression() throws Exception { + Argument locationArgument = new Argument(); + locationArgument.setRefEntityKey(new ReferencedEntityKey("location", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationArgument.setDefaultValue("unknown"); + originatorId = customerId; + + Argument locationFilterArgument = new Argument(); + locationFilterArgument.setRefEntityKey(new ReferencedEntityKey("locationFilter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationFilterArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + locationFilterArgument.setDefaultValue("None"); + + Map arguments = Map.of( + "location", locationArgument, + "locationFilter", locationFilterArgument + ); + + Map createRules = Map.of( + AlarmSeverity.INDETERMINATE, new Condition(createSimpleExpression( + "location", StringOperation.CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null) + ); + Condition clearRule = new Condition(createSimpleExpression( + "location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null); + + CalculatedField calculatedField = createAlarmCf(customerId, "New resident", + arguments, createRules, clearRule); + + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}"); + loginTenantAdmin(); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_dynamicSchedule() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument scheduleArgument = new Argument(); + scheduleArgument.setRefEntityKey(new ReferencedEntityKey("schedule", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + scheduleArgument.setDefaultValue("None"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "schedule", scheduleArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null, null, + new AlarmConditionValue<>(null, "schedule")) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + String schedule = """ + {"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]} + """; + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + postTelemetry(deviceId, "{\"temperature\":50}"); + + Thread.sleep(1000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + // checking multiple debug events due to scheduled reevaluation (which also produces debug events) + CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeAlarmType() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + calculatedField.setName("New alarm type"); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRuleExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + ((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression()) + .setExpression("return temperature >= 50;"); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRequiredEventsCountForRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < eventsCountMajor; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + + // decreasing required events count for critical rule + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + ((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition()) + .setCount(new AlarmConditionValue<>(6, null)); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + } + + @Test + public void testChangeConditionArgumentSource() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("100"); + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":100}"); + loginTenantAdmin(); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + // not created because tenant's threshold 100 is used + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold") + .setRefDynamicSourceConfiguration(null); + // using threshold 50 on device level + calculatedField = saveCalculatedField(calculatedField); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testAlarmDetails() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "humidity", humidityArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) + ); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", + arguments, createRules, null, configuration -> { + configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "temperature is ${temperature}, humidity is ${humidity}" + ); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("temperature is 50, humidity is 50"); + }); + + ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "UPDATED temperature is ${temperature}, humidity is ${humidity}" + ); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isFalse(); + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("UPDATED temperature is 50, humidity is 50"); + }); + } + + @Test + public void testCreateAlarm_scheduleStarted() throws Exception { + Argument parkingSpotOccupiedArgument = new Argument(); + parkingSpotOccupiedArgument.setRefEntityKey(new ReferencedEntityKey("parkingSpotOccupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + parkingSpotOccupiedArgument.setDefaultValue("false"); + Map arguments = Map.of( + "parkingSpotOccupied", parkingSpotOccupiedArgument + ); + + SpecificTimeSchedule schedule = new SpecificTimeSchedule(); + schedule.setTimezone(ZoneId.systemDefault().getId()); + schedule.setDaysOfWeek(Set.of(1, 2, 3, 4, 5, 6, 7)); + long startsOn = Duration.between(LocalDate.now().atStartOfDay(), LocalDateTime.now()) + .plus(15, ChronoUnit.SECONDS).toMillis(); + schedule.setStartsOn(startsOn); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return parkingSpotOccupied == true;", null, null, null, + new AlarmConditionValue<>(schedule, null)) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm", + arguments, createRules, null); + + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}"); + + Thread.sleep(10000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testManualClearAlarm() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }).getAlarm(); + + doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class); + Thread.sleep(1000); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId()); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + // TODO: MSA tests + + private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer assertion) { + return checkAlarmResult(calculatedField, null, assertion); + } + + private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, + Predicate waitFor, + Consumer assertion) { + TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getLatestAlarmResult(calculatedField.getId()), result -> + result != null && (waitFor == null || waitFor.test(result))); + assertion.accept(alarmResult); + + Alarm alarm = alarmResult.getAlarm(); + assertThat(alarm.getOriginator()).isEqualTo(originatorId); + assertThat(alarm.getType()).isEqualTo(calculatedField.getName()); + return alarmResult; + } + + private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) { + List debugEvents = getDebugEvents(calculatedFieldId, 1); + if (debugEvents.isEmpty()) { + return null; + } + CalculatedFieldDebugEvent debugEvent = debugEvents.get(0); + if (debugEvent.getError() != null) { + throw new RuntimeException(debugEvent.getError()); + } + if (debugEvent.getId().equals(latestEventId)) { + return null; + } + latestEventId = debugEvent.getId(); + return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + } + + private CalculatedField createAlarmCf(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { + Map createRules = new HashMap<>(); + createConditions.forEach((severity, condition) -> { + createRules.put(severity, toAlarmRule(condition)); + }); + AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setName(alarmType); + calculatedField.setType(CalculatedFieldType.ALARM); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + configuration.setCreateRules(createRules); + configuration.setClearRule(clearRule); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + if (modifier.length > 0) { + modifier[0].accept(configuration); + } + CalculatedField savedCalculatedField = saveCalculatedField(calculatedField); + + CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getDebugEvents(savedCalculatedField.getId(), 1), + events -> !events.isEmpty()).get(0); + latestEventId = debugEvent.getId(); + return savedCalculatedField; + } + + private AlarmRule toAlarmRule(Condition condition) { + AlarmRule rule = new AlarmRule(); + AlarmConditionExpression expression; + if (condition.getTbelExpression() != null) { + TbelAlarmConditionExpression tbelExpression = new TbelAlarmConditionExpression(); + tbelExpression.setExpression(condition.getTbelExpression()); + expression = tbelExpression; + } else { + expression = condition.getSimpleExpression(); + } + if (condition.getEventsCount() != null) { + RepeatingAlarmCondition alarmCondition = new RepeatingAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setCount(condition.getEventsCount()); + rule.setCondition(alarmCondition); + } else if (condition.getDuration() != null) { + DurationAlarmCondition alarmCondition = new DurationAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setUnit(TimeUnit.MILLISECONDS); + alarmCondition.setValue(condition.getDuration()); + rule.setCondition(alarmCondition); + } else { + SimpleAlarmCondition alarmCondition = new SimpleAlarmCondition(); + alarmCondition.setExpression(expression); + rule.setCondition(alarmCondition); + } + if (condition.getSchedule() != null) { + rule.getCondition().setSchedule(condition.getSchedule()); + } + return rule; + } + + private SimpleAlarmConditionExpression createSimpleExpression(String argument, StringOperation stringOperation, AlarmConditionValue conditionValue) { + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument(argument); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(stringOperation); + predicate.setValue(conditionValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + return simpleExpression; + } + + private List getDebugEvents(CalculatedFieldId calculatedFieldId, int limit) { + return eventDao.findLatestEvents(tenantId.getId(), calculatedFieldId.getId(), EventType.DEBUG_CALCULATED_FIELD, limit).stream() + .map(e -> (CalculatedFieldDebugEvent) e).toList(); + } + + @Getter + @AllArgsConstructor + private static final class Condition { + + private final String tbelExpression; + private final SimpleAlarmConditionExpression simpleExpression; + private AlarmConditionValue eventsCount; + private AlarmConditionValue duration; + private AlarmConditionValue schedule; + + private Condition(String tbelExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = tbelExpression; + this.simpleExpression = null; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + private Condition(SimpleAlarmConditionExpression simpleExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = null; + this.simpleExpression = simpleExpression; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java new file mode 100644 index 0000000000..d2f9621064 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java @@ -0,0 +1,200 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest { + + public static final int TIMEOUT = 60; + public static final int POLL_INTERVAL = 1; + + @Test + public void testCreateCFWithCurrentOwner() throws Exception { + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + Device testDevice = createDevice("Test device", "1234567890"); + + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":10}"); + + await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("110"); + }); + } + + @Test + public void testChangeOwner() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + doDelete("/api/customer/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + await().alias("change owner -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + } + + @Test + public void testCreateCFWithCurrentOwnerWhenEntityIsProfile() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); + + Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset1.getId().getId()).andExpect(status().isOk()); + + Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); // owner - TENANT + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(assetProfile.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 1 + ObjectNode result1 = getLatestTelemetry(asset1.getId(), "result"); + assertThat(result1).isNotNull(); + assertThat(result1.get("result").get(0).get("value").asText()).isEqualTo("105"); + + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset2.getId().getId()).andExpect(status().isOk()); + + await().alias("change asset2 owner -> recalculate state for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + } + + private CalculatedField buildCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("Test Calculated Field"); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("attrKey", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument.setRefEntityKey(refEntityKey); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + + config.setArguments(Map.of("a", argument)); + + config.setExpression("a + 100"); + + Output output = new Output(); + output.setName("result"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + config.setOutput(output); + + calculatedField.setConfiguration(config); + return calculatedField; + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + +} 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 a805242e85..b6b006b16c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -17,14 +17,15 @@ package org.thingsboard.server.cf; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; -import org.junit.jupiter.api.BeforeEach; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; @@ -69,15 +71,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public static final int TIMEOUT = 60; public static final int POLL_INTERVAL = 1; - @BeforeEach - void setUp() throws Exception { - loginTenantAdmin(); - } - @Test public void testSimpleCalculatedFieldWhenAllTelemetryPresent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); CalculatedField calculatedField = new CalculatedField(); @@ -114,7 +111,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -135,6 +132,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0"); }); @@ -199,7 +197,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -248,7 +246,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -433,7 +431,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testSimpleCalculatedFieldWhenExpressionIsInvalid() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -469,7 +467,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -484,7 +482,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes 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))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -528,10 +526,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes 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))); + postTelemetry(testDevice.getId(), 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))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -572,7 +570,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); long tsABeforeTsB = tsB - 300L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":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) @@ -588,7 +586,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes 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))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -1002,6 +1000,166 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testPropagationCalculatedField_withExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device With Expression", "sn-prop-1"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + doPost("/api/relation", rel1).andExpect(status().isOk()); + doPost("/api/relation", rel2).andExpect(status().isOk()); + + // Telemetry on device + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk()); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("t", arg)); + + cfg.setExpression("{\"testResult\": t * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNotNull(); + assertThat(attrs2).isNotNull(); + assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/SERVER_SCOPE?keys=testResult").andExpect(status().isOk()); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"temperature\":25}")).andExpect(status().isOk()); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNullOrEmpty(); + assertThat(attrs2).isNotNull(); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50); + }); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device Without Expression", "sn-prop-2"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + doPost("/api/relation", rel1).andExpect(status().isOk()); + doPost("/api/relation", rel2).andExpect(status().isOk()); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts)); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temperatureComputed", arg)); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); + + // Update telemetry on device + long newTs = System.currentTimeMillis() - 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs)); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + }); + } + @Test public void testCalculatedFieldWhenTheSameTelemetryKeysUsed() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); diff --git a/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java new file mode 100644 index 0000000000..48c4e67608 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java @@ -0,0 +1,846 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cf; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; + +@DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest { + + private Tenant savedTenant; + + private DeviceProfile deviceProfile; + private Device device1; + private String accessToken1 = "1234567890111"; + private Device device2; + private String accessToken2 = "1234567890222"; + + private AssetProfile assetProfile; + private Asset asset; + + private final long deduplicationInterval = 5; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); + tenantProfileConfig.setMinAllowedScheduledUpdateIntervalInSecForCF(1); + }); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant@thingsboard.org"); + tenantAdmin.setFirstName("John"); + tenantAdmin.setLastName("Doe"); + + createUserAndLogin(tenantAdmin, "testPassword"); + + deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Device Profile"), DeviceProfile.class); + device1 = createDevice("Device 1", deviceProfile.getId(), accessToken1); + device2 = createDevice("Device 2", deviceProfile.getId(), accessToken2); + + postTelemetry(device1.getId(), "{\"occupied\":true}"); + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + assetProfile = doPost("/api/assetProfile", createAssetProfile("Asset Profile"), AssetProfile.class); + asset = createAsset("Asset", assetProfile.getId()); + + createEntityRelation(asset.getId(), device1.getId(), "Contains"); + createEntityRelation(asset.getId(), device2.getId(), "Contains"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + deleteTenant(savedTenant.getId()); + } + + @Test + public void testCreateCfOnProfile_checkInitialAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testAddEntityToProfile_checkAggregation() throws Exception { + createOccupancyCF(assetProfile.getId()); + + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + postTelemetry(device4.getId(), "{\"occupied\":true}"); + + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + + await().alias("add entity to profile with no related entities and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode occupancy = getLatestTelemetry(asset2.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").isNull()).isTrue(); + }); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + await().alias("create relations and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testChangeEntityProfile_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + AssetProfile newAssetProfile = createAssetProfile("New Asset Profile"); + asset2.setAssetProfileId(newAssetProfile.getId()); + doPost("/api/asset", asset2, Asset.class); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("change profile and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfOnAssetAndNoTelemetryOnDevices_checkDefaultValueUsed() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndUpdateTelemetry_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndRelationToRuleChain_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("RuleChain"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), ruleChain.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry on rule chain and no aggregation performed").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteCf_checkNoAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/calculatedField/" + cf.getId().getId().toString()) + .andExpect(status().isOk()); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("delete cf and update telemetry and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateTelemetry_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry -> no changes").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + await().alias("create CF and perform initial calculation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteTelemetry_checkAggregationWithPreviousValuesOrDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + long currentTime = System.currentTimeMillis(); + long firstTs = currentTime - 10; + long secondTs = currentTime - 10; + long thirdTs = currentTime - 5; + postTelemetry(device3.getId(), "{\"ts\": " + firstTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device4.getId(), "{\"ts\": " + secondTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device3.getId(), "{\"ts\": " + thirdTs + ", \"values\": {\"occupied\":true}}"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + thirdTs + "&endTs=" + thirdTs + 1, String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + secondTs + "&endTs=" + secondTs + 1, String.class); + + await().alias("delete latest telemetry and perform aggregation with previous or default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteAttr_checkAggregationWithDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + postAttributes(device3.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + postAttributes(device4.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + + createOccupancyCFWithAttr(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + + await().alias("delete attribute and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + Device device3 = createDevice("Device 3", deviceProfile.getId(), "1234567890333"); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + createEntityRelation(asset.getId(), device3.getId(), "Contains"); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "2", + "totalSpaces", "3" + )); + }); + } + + @Test + public void testDeleteRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + deleteEntityRelation(new EntityRelation(asset.getId(), device1.getId(), "Contains", RelationTypeGroup.COMMON)); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteEntityByRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/device/" + device1.getId()).andExpect(status().isOk()); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateRelationPath_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + Device device3 = createDevice("Device 3", "1234567890333"); + createEntityRelation(asset.getId(), device3.getId(), "Has"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, "Has")); + saveCalculatedField(cf); + + await().alias("update relation path and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateArguments_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupiedStatus\":false}"); + postTelemetry(device2.getId(), "{\"occupiedStatus\":false}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("oc", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + configuration.setArguments(Map.of("oc", argument)); + saveCalculatedField(cf); + + await().alias("update arguments and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateMetrics_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + AggMetric aggMetric = new AggMetric(); + aggMetric.setInput(new AggKeyInput("temp")); + aggMetric.setFilter("return temp < 100;"); + aggMetric.setFunction(AggFunction.MAX); + configuration.setMetrics(Map.of("maxTemperature", aggMetric)); + saveCalculatedField(cf); + + await().alias("update metrics and perform aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24")); + }); + + postTelemetry(device1.getId(), "{\"temperature\":101.3}"); + postTelemetry(device2.getId(), "{\"temperature\":25.8}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26")); + }); + } + + @Test + public void testUpdateOutput_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + configuration.setOutput(output); + saveCalculatedField(cf); + + await().alias("update output and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode avgTemperature = getServerAttributes(asset.getId(), "avgTemperature"); + assertThat(avgTemperature).isNotNull(); + assertThat(avgTemperature.get(0)).isNotNull(); + assertThat(avgTemperature.get(0).get("value").asText()).isEqualTo("24.2"); + }); + } + + @Test + public void testUpdateDeduplicationInterval_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setDeduplicationIntervalInSec(2 * deduplicationInterval); + saveCalculatedField(cf); + + await().alias("update deduplication interval and perform aggregation").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + postTelemetry(device2.getId(), "{\"temperature\":32.1}"); + + await().alias("update telemetry and perform aggregation").atMost(2 * deduplicationInterval + 10, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "28")); + }); + } + + private void checkInitialCalculation() { + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + } + + private void checkInitialCalculationValues() throws Exception { + ObjectNode occupancy = getLatestTelemetry(asset.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + } + + private CalculatedField createAvgTemperatureCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("20"); + arguments.put("temp", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric avgMetric = new AggMetric(); + avgMetric.setFunction(AggFunction.AVG); + avgMetric.setFilter("return temp >= 20;"); + avgMetric.setInput(new AggKeyInput("temp")); + aggMetrics.put("avgTemperature", avgMetric); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Average temperature", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCFWithAttr(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createAggCf(String name, + EntityId entityId, + RelationPathLevel relation, + Map inputs, + Map metrics, + Output output) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(name); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + configuration.setRelation(relation); + configuration.setArguments(inputs); + configuration.setDeduplicationIntervalInSec(deduplicationInterval); + configuration.setScheduledUpdateInterval(10); + configuration.setMetrics(metrics); + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + + private Device createDevice(String name, DeviceProfileId deviceProfileId, String accessToken) { + Device device = new Device(); + device.setName(name); + device.setDeviceProfileId(deviceProfileId); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + + private void verifyTelemetry(EntityId entityId, Map expectedResults) throws Exception { + ObjectNode result = getLatestTelemetry(entityId, expectedResults.keySet().toArray(new String[0])); + assertThat(result).isNotNull(); + expectedResults.forEach((key, value) -> assertThat(result.get(key).get(0).get("value").asText()).isEqualTo(value)); + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private ArrayNode getServerAttributes(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/attributes/SERVER_SCOPE?keys=" + String.join(",", keys), ArrayNode.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 01c24ad493..d4eec0d648 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -128,11 +128,17 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(HasName entity, EntityId entityId, EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(tenantId, entity, entityId, originatorId, tenantId, customerId, userId, userName, actionType, actionTypeEdge, additionalInfo); + } + + protected void testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(TenantId entityTenantId, HasName entity, EntityId entityId, EntityId originatorId, + TenantId authTenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { int cntTime = 1; - testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionTypeEdge, cntTime); - testLogEntityActionEntityEqClass(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); + testNotificationMsgToEdgeServiceTime(entityId, entityTenantId, actionTypeEdge, cntTime); + testLogEntityActionEntityEqClass(entity, originatorId, authTenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); - testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); + testPushMsgToRuleEngineTime(matcherOriginatorId, authTenantId, entity, cntTime); Mockito.reset(tbClusterService, auditLogService); } @@ -159,17 +165,26 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, int cntTime, int cntTimeEdge, int cntTimeRuleEngine, Object... additionalInfo) { + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, entity, originator, tenantId, customerId, userId, userName, actionType, + cntTime, cntTimeEdge, cntTimeRuleEngine, additionalInfo); + } + + protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(TenantId entityTenantId, HasName entity, HasName originator, + TenantId authTenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, + int cntTime, int cntTimeEdge, int cntTimeRuleEngine, Object... additionalInfo) { EntityId originatorId = createEntityId_NULL_UUID(originator); - testSendNotificationMsgToEdgeServiceTimeEntityEqAny(tenantId, actionType, cntTimeEdge); + testSendNotificationMsgToEdgeServiceTimeEntityEqAny(entityTenantId, actionType, cntTimeEdge); ArgumentMatcher matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass()); ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass()); ArgumentMatcher matcherCustomerId = customerId == null ? argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId); ArgumentMatcher matcherUserId = userId == null ? argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId); - testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, + testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, authTenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, extractMatcherAdditionalInfoClass(additionalInfo)); - testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTimeRuleEngine); + testPushMsgToRuleEngineTime(matcherOriginatorId, authTenantId, entity, cntTimeRuleEngine); + } protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(HasName entity, HasName originator, diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index cf9d2feb23..89b2681015 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -15,19 +15,13 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.event.EventType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.dao.rule.RuleChainService; @@ -61,18 +55,6 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class); } - protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { - return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE.getOldName(), limit); - } - - protected PageData getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception { - TimePageLink pageLink = new TimePageLink(limit); - return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", - new TypeReference>() { - }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); - } - - protected JsonNode getMetadata(EventInfo outEvent) { String metaDataStr = outEvent.getBody().get("metadata").asText(); try { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index f04cc049c8..ae7bbfe2e8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -77,11 +77,14 @@ import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResourceInfo; @@ -89,6 +92,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -101,6 +106,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; @@ -1068,6 +1074,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/relation", relation); } + protected void deleteEntityRelation(EntityRelation entityRelation) throws Exception { + String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + entityRelation.getFrom().getId(), + entityRelation.getFrom().getEntityType(), + entityRelation.getType(), + entityRelation.getTo().getId(), + entityRelation.getTo().getEntityType()); + doDelete(url); + } + protected List findRelationsByTo(EntityId entityId) throws Exception { String url = String.format("/api/relations?toId=%s&toType=%s", entityId.getId(), entityId.getEntityType().name()); MvcResult mvcResult = doGet(url).andReturn(); @@ -1325,4 +1341,34 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk()); } + protected void postTelemetry(EntityId entityId, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { + return doPost("/api/calculatedField", calculatedField, CalculatedField.class); + } + + protected PageData getCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + } + + protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { + return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE, limit); + } + + protected PageData getEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit) throws Exception { + TimePageLink pageLink = new TimePageLink(limit); + return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", + new TypeReference>() { + }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 27622b347a..4ebace6ae7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -28,13 +28,16 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.security.Authority; @@ -44,6 +47,7 @@ import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @@ -81,7 +85,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { @Test public void testSaveCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -109,7 +113,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { @Test public void testSaveGeofencingCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig()); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.GEOFENCING); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -134,10 +138,48 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testSavePropagationCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getPropagationCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSavePropagationCalculatedFieldWithNullArguments() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION, getPropagationCalculatedFieldConfig(null)); + + doPost("/api/calculatedField", calculatedField) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("arguments must not be empty"))); + } + @Test public void testGetCalculatedFieldById() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); CalculatedField fetchedCalculatedField = doGet("/api/calculatedField/" + savedCalculatedField.getId().getId(), CalculatedField.class); @@ -149,10 +191,22 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testGetCalculatedFields() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(getCalculatedFields(testDevice.getId(), null, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + assertThat(getCalculatedFields(testDevice.getId(), CalculatedFieldType.SIMPLE, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + } + @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -163,17 +217,27 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { doGet("/api/calculatedField/" + savedCalculatedField.getId().getId()).andExpect(status().isNotFound()); } - private CalculatedField getCalculatedField(DeviceId deviceId) { - return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig()); + private CalculatedField getSimpleCalculatedField(EntityId entityId) { + return getCalculatedField(entityId, CalculatedFieldType.SIMPLE); } - private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) { + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType) { + return getCalculatedField(entityId, cfType, null); + } + + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType, CalculatedFieldConfiguration customConfiguration) { CalculatedField calculatedField = new CalculatedField(); - calculatedField.setEntityId(deviceId); - calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setEntityId(entityId); + calculatedField.setType(cfType); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(configuration); + if (customConfiguration != null) { + calculatedField.setConfiguration(customConfiguration); + } else switch (cfType) { + case SIMPLE -> calculatedField.setConfiguration(getSimpleCalculatedFieldConfig()); + case GEOFENCING -> calculatedField.setConfiguration(getGeofencingCalculatedFieldConfig()); + case PROPAGATION -> calculatedField.setConfiguration(getPropagationCalculatedFieldConfig()); + } calculatedField.setVersion(1L); return calculatedField; } @@ -198,6 +262,31 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { return config; } + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig() { + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + return getPropagationCalculatedFieldConfig(Map.of("t", arg)); + } + + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig(Map arguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + config.setApplyExpressionToResolvedArguments(false); + config.setExpression(null); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(arguments); + + return config; + } + private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 352e1fcfeb..d88aaa2756 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -943,7 +943,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { private List loadLwm2mResources() throws Exception { - var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303"); + var models = List.of("1", "2", "3-1_2", "5", "6", "9", "19", "3303"); List resources = new ArrayList<>(models.size()); diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index d7aa6f5770..28d43699fd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -116,7 +116,7 @@ public class UserControllerTest extends AbstractControllerTest { foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); Assert.assertEquals(foundUser, savedUser); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(user.getTenantId(), foundUser, foundUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); @@ -155,7 +155,7 @@ public class UserControllerTest extends AbstractControllerTest { doDelete("/api/user/" + savedUser.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(foundUser, foundUser.getId(), foundUser.getId(), + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(user.getTenantId(), foundUser, foundUser.getId(), foundUser.getId(), SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.DELETED, ActionType.DELETED, SYSTEM_TENANT.getId().toString()); } @@ -284,6 +284,26 @@ public class UserControllerTest extends AbstractControllerTest { ActionType.ADDED, new DataValidationException(msgError)); } + @Test + public void testShouldNotDeleteLastTenantAdmin() throws Exception { + loginSysAdmin(); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(tenantId); + tenantAdmin2.setEmail("tenant2@thingsboard.io"); + tenantAdmin2 = doPost("/api/user", tenantAdmin2, User.class); + + // delete second tenant admin - ok + doDelete("/api/user/" + tenantAdmin2.getId().getId().toString()) + .andExpect(status().isOk()); + + // delete last tenant admin - forbidden + doDelete("/api/user/" + tenantAdminUser.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("At least one tenant administrator must remain!"))); + } + @Test public void testSaveUserWithInvalidEmail() throws Exception { loginSysAdmin(); @@ -394,7 +414,7 @@ public class UserControllerTest extends AbstractControllerTest { User testManyUser = new User(); testManyUser.setTenantId(tenantId); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.ADDED, cntEntity, cntEntity, cntEntity); @@ -506,7 +526,7 @@ public class UserControllerTest extends AbstractControllerTest { } User testManyUser = new User(); testManyUser.setTenantId(tenantId); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.DELETED, cntEntity, NUMBER_OF_USERS, cntEntity, ""); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 3e29f212f0..fb99d6ad0e 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -118,7 +118,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac .pollInterval(10, MILLISECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { - List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT.getOldName(), 1000) + List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT, 1000) .getData().stream().filter(e -> { var body = e.getBody(); return body.has("event") && body.get("event").asText().equals("STARTED") diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 1f1ee32df2..d144fe4dcc 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -20,9 +20,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; @@ -44,10 +46,11 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -92,13 +95,17 @@ public class GeofencingCalculatedFieldStateTest { private ApiLimitService apiLimitService; @Mock private RelationService relationService; + @InjectMocks + private ActorSystemContext systemContext; @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, relationService); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new GeofencingCalculatedFieldState(ctx.getArgNames()); + state = new GeofencingCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(); } @Test @@ -114,7 +121,7 @@ public class GeofencingCalculatedFieldStateTest { )); Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -128,21 +135,21 @@ public class GeofencingCalculatedFieldStateTest { @Test void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() { - assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); } @Test void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { - assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); } @Test void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { - assertThatThrownBy(() -> state.updateState(ctx, Map.of("someArgumentName", latitudeArgEntry))) + assertThatThrownBy(() -> state.update(Map.of("someArgumentName", latitudeArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed."); } @@ -153,7 +160,7 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L); Map newArgs = Map.of("latitude", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).isEqualTo(newArgs); @@ -165,7 +172,7 @@ public class GeofencingCalculatedFieldStateTest { Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isFalse(); assertThat(state.getArguments()).isEqualTo(newArgs); @@ -175,7 +182,7 @@ public class GeofencingCalculatedFieldStateTest { void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() { state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); - assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING"); } @@ -185,7 +192,7 @@ public class GeofencingCalculatedFieldStateTest { void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() { state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); - assertThatThrownBy(() -> state.updateState(ctx, Map.of("allowedZones", latitudeArgEntry))) + assertThatThrownBy(() -> state.update(Map.of("allowedZones", latitudeArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); } @@ -193,31 +200,31 @@ public class GeofencingCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, "allowedZones", geofencingAllowedZoneArgEntry, "restrictedZones", geofencingRestrictedZoneArgEntry - )); + ), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, "allowedZones", geofencingAllowedZoneArgEntry, - "restrictedZones", geofencingRestrictedZoneArgEntry - )); - - state.getArguments().put("noParkingZones", new GeofencingArgumentEntry()); - + "restrictedZones", new GeofencingArgumentEntry() + ), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); } @Test @@ -235,7 +242,7 @@ public class GeofencingCalculatedFieldStateTest { when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -251,9 +258,9 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); - CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result2 = performCalculation(); assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); @@ -310,7 +317,7 @@ public class GeofencingCalculatedFieldStateTest { when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -323,9 +330,9 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); - CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result2 = performCalculation(); assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); @@ -380,7 +387,7 @@ public class GeofencingCalculatedFieldStateTest { when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -395,9 +402,9 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); - CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result2 = performCalculation(); assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); @@ -475,4 +482,8 @@ public class GeofencingCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java new file mode 100644 index 0000000000..14a1b629c1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.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.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PropagationArgumentEntryTest { + + private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); + private final AssetId ENTITY_2_ID = new AssetId(UUID.fromString("7bd85073-ded5-414f-a2ef-bd56ad3dbf6a")); + private final AssetId ENTITY_3_ID = new AssetId(UUID.fromString("d64f3e51-2ec2-472f-b475-b095ef8bdc70")); + + private PropagationArgumentEntry entry; + + @BeforeEach + void setUp() { + List propagationEntityIds = new ArrayList<>(); + propagationEntityIds.add(ENTITY_1_ID); + propagationEntityIds.add(ENTITY_2_ID); + entry = new PropagationArgumentEntry(propagationEntityIds); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.PROPAGATION); + } + + @Test + void testIsEmpty() { + PropagationArgumentEntry emptyEntry = new PropagationArgumentEntry(List.of()); + assertThat(emptyEntry.isEmpty()).isTrue(); + } + + @Test + void testGetValueReturnsPropagationIds() { + assertThat(entry.getValue()).isInstanceOf(List.class); + @SuppressWarnings("unchecked") + List value = (List) entry.getValue(); + assertThat(value).containsExactly(ENTITY_1_ID, ENTITY_2_ID); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryReplacesWithNewIds() { + var newIds = new ArrayList(List.of(ENTITY_3_ID, ENTITY_1_ID)); + var updated = new PropagationArgumentEntry(newIds); + + boolean changed = entry.updateEntry(updated); + + assertThat(changed).isTrue(); + assertThat(entry.getPropagationEntityIds()).containsExactlyElementsOf(newIds); + } + + @Test + void testUpdateEntryClearsWhenNewEntryIsEmpty() { + var updatedEmpty = new PropagationArgumentEntry(List.of()); + + boolean changed = entry.updateEntry(updatedEmpty); + + assertThat(changed).isTrue(); + assertThat(entry.getPropagationEntityIds()).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithValues() { + TbelCfArg arg = entry.toTbelCfArg(); + assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); + assertThat((List) tbelCfPropagationArg.getValue()).containsExactly(ENTITY_1_ID, ENTITY_2_ID); + } + + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithEmptyValues() { + var empty = new PropagationArgumentEntry(List.of()); + TbelCfArg emptyArg = empty.toTbelCfArg(); + assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); + assertThat((List) tbelCfPropagationArg.getValue()).isEmpty(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java new file mode 100644 index 0000000000..ddb9f378b0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -0,0 +1,249 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) +public class PropagationCalculatedFieldStateTest { + + private static final String TEMPERATURE_ARGUMENT_NAME = "t"; + private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; + private static final double TEMPERATURE_VALUE = 12.5; + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); + private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); + private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); + + private final SingleValueArgumentEntry singleValueArgEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); + + private final PropagationArgumentEntry propagationArgEntry = + new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); + + private PropagationCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Autowired + private TbelInvokeService tbelInvokeService; + + @MockitoBean + private ApiLimitService apiLimitService; + + @MockitoBean + private ActorSystemContext actorSystemContext; + + @BeforeEach + void setUp() { + when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + } + + void initCtxAndState(boolean applyExpressionToResolvedArguments) { + ctx = new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext); + ctx.init(); + + state = new PropagationCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(); + } + + @Test + void testType() { + initCtxAndState(false); + assertThat(state.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void testInitAddsRequiredArgument() { + initCtxAndState(false); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyReturnFalseWhenNoArgumentsSet() { + initCtxAndState(false); + assertThat(state.isReady()).isFalse(); + } + + @Test + void testIsReadyWhenPropagationArgIsNull() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyWhenPropagationArgIsEmpty() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyWhenPropagationArgHasEntities() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + + @Test + void testPerformCalculationWithEmptyPropagationArg() throws Exception { + initCtxAndState(false); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); + + PropagationCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.isEmpty()).isTrue(); + assertThat(result.getPropagationEntityIds()).isNullOrEmpty(); + } + + @Test + void testPerformCalculationWithArgumentsOnlyMode() throws Exception { + initCtxAndState(false); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + JacksonUtil.addKvEntry(expectedNode, singleValueArgEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + @Test + void testPerformCalculationWithExpressionResultMode() throws Exception { + initCtxAndState(true); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + expectedNode.put(TEST_RESULT_EXPRESSION_KEY, TEMPERATURE_VALUE * 2); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.PROPAGATION); + calculatedField.setName("Test Propagation CF"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(getCalculatedFieldConfig(applyExpressionToResolvedArguments)); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments); + + Argument temperatureArg = new Argument(); + ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + temperatureArg.setRefEntityKey(tempKey); + + config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); + config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + config.setOutput(output); + + return config; + } + + private PropagationCalculatedFieldResult performCalculation() throws ExecutionException, InterruptedException { + return (PropagationCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java new file mode 100644 index 0000000000..cc60b249ac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RelatedEntitiesArgumentEntryTest { + + private RelatedEntitiesArgumentEntry entry; + + private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); + private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); + + private final long ts = System.currentTimeMillis(); + + @BeforeEach + void setUp() { + Map aggInputs = new HashMap<>(); + aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L))); + aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L))); + + entry = new RelatedEntitiesArgumentEntry(aggInputs, false); + } + + @Test + void testUpdateEntryWhenNotAggEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); + } + + @Test + void testUpdateEntryWhenAggArgumentEntryPasser() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + DeviceId device4 = new DeviceId(UUID.randomUUID()); + + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = new RelatedEntitiesArgumentEntry(Map.of( + device3, new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 16L), 13L)), + device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) + ), false); + + assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(4); + assertThat(aggInputs.get(device3)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device3)); + assertThat(aggInputs.get(device4)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device4)); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndNoEntriesById() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(3); + assertThat(aggInputs.get(device3)).isEqualTo(singleEntityArgumentEntry); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(2); + assertThat(aggInputs.get(device2)).isEqualTo(singleEntityArgumentEntry); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 8c714bc0e7..51e633a232 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -18,12 +18,14 @@ package org.thingsboard.server.service.cf.ctx.state; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -41,10 +43,10 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; @@ -77,10 +79,16 @@ public class ScriptCalculatedFieldStateTest { @BeforeEach void setUp() { + ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); + when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(systemContext.getApiLimitService()).thenReturn(apiLimitService); + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService, null); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new ScriptCalculatedFieldState(ctx.getArgNames()); + state = new ScriptCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(); } @Test @@ -93,7 +101,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -110,7 +118,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -125,7 +133,7 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -141,7 +149,7 @@ public class ScriptCalculatedFieldStateTest { "assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L) )); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -153,20 +161,21 @@ public class ScriptCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("deviceTemperature"); } private TsRollingArgumentEntry createRollingArgEntry() { @@ -221,4 +230,8 @@ public class ScriptCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 3aef8896de..a64f1e4c60 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -18,9 +18,11 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -39,8 +41,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -67,13 +70,17 @@ public class SimpleCalculatedFieldStateTest { @Mock private ApiLimitService apiLimitService; + @InjectMocks + private ActorSystemContext systemContext; @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, null); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new SimpleCalculatedFieldState(ctx.getArgNames()); + state = new SimpleCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(); } @Test @@ -89,7 +96,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -107,7 +114,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -121,7 +128,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.updateState(ctx, newArgs)) + assertThatThrownBy(() -> state.update(newArgs, ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument type detected for argument: key3. " + "Rolling argument entry is not supported for simple calculated fields."); @@ -135,7 +142,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -152,7 +159,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx.getEntityId(), ctx)) + assertThatThrownBy(() -> state.performCalculation(Collections.emptyMap(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -165,7 +172,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -186,7 +193,7 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -197,28 +204,29 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, "key2", key2ArgEntry, "key3", key3ArgEntry - )); - + ), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, - "key2", key2ArgEntry - )); - state.getArguments().put("key3", new SingleValueArgumentEntry()); - + "key2", key2ArgEntry, + "key3", new SingleValueArgumentEntry() + ), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("key3"); } private CalculatedField getCalculatedField() { @@ -266,4 +274,8 @@ public class SimpleCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file 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 5ea3808468..4ada355054 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 @@ -57,6 +57,11 @@ public class SingleValueArgumentEntryTest { assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse(); } + @Test + void testUpdateEntryWithTheSameTsAndDifferentVersion() { + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L))).isTrue(); + } + @Test void testUpdateEntryWhenNewVersionIsNull() { assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue(); @@ -115,4 +120,5 @@ public class SingleValueArgumentEntryTest { expectedList.add(Map.of("test2", 20)); assertThat(singleValueArg.getValue()).isEqualTo(expectedList); } + } 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 0069310b5d..7806215a51 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 @@ -98,7 +98,7 @@ public class JobManagerTest extends AbstractControllerTest { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING); - assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getSuccessfulCount()).isBetween(0, tasksCount - 1); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); }); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java index 59093ad802..c1b8f911ff 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java @@ -27,17 +27,16 @@ import org.thingsboard.server.dao.service.DaoSqlTest; public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest { /* - * Some tests are overridden because they are based on - * tenant partitioning strategy (subsequent tasks processing within a tenant) - * */ + * Some tests are overridden because they are based on + * tenant partitioning strategy (subsequent tasks processing within a tenant) + * */ @Override - public void testCancelJob_simulateTaskProcessorRestart() throws Exception { + public void testCancelJob_simulateTaskProcessorRestart() { } @Override public void testSubmitJob_generalError() { - } } 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 659f6379e7..c3a2119cbc 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 @@ -27,7 +27,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.limits.RateLimitService; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -39,17 +39,19 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; -import org.thingsboard.server.common.data.device.profile.AlarmCondition; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -import org.thingsboard.server.common.data.device.profile.AlarmRule; -import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; -import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -87,9 +89,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; @@ -106,12 +105,10 @@ import org.thingsboard.server.service.system.DefaultSystemInfoService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -193,7 +190,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_alarmTrigger() throws Exception { String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + - "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${info.data}."; + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${details.data}."; String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); @@ -234,8 +231,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { }); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(10, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -341,8 +338,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); getWsClient().registerWaitForUpdate(); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(10, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -944,35 +941,28 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); deviceProfile.setTenantId(tenantId); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); - List alarms = new ArrayList<>(); - DeviceProfileAlarm alarm = new DeviceProfileAlarm(); - alarm.setAlarmType(alarmType); - alarm.setId(alarmType); + CalculatedField alarmCf = new CalculatedField(); + alarmCf.setType(CalculatedFieldType.ALARM); + alarmCf.setEntityId(deviceProfile.getId()); + alarmCf.setName(alarmType); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + configuration.setArguments(Map.of("createAlarm", argument)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setAlarmDetails("attribute is ${bool}"); - AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setSpec(new SimpleAlarmConditionSpec()); - List condition = new ArrayList<>(); - - AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); - alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "bool")); - BooleanFilterPredicate predicate = new BooleanFilterPredicate(); - predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - predicate.setValue(new FilterPredicateValue<>(true)); - - alarmConditionFilter.setPredicate(predicate); - alarmConditionFilter.setValueType(EntityKeyValueType.BOOLEAN); - condition.add(alarmConditionFilter); - alarmCondition.setCondition(condition); - alarmRule.setCondition(alarmCondition); - TreeMap createRules = new TreeMap<>(); - createRules.put(AlarmSeverity.CRITICAL, alarmRule); - alarm.setCreateRules(createRules); - alarms.add(alarm); - - deviceProfile.getProfileData().setAlarms(alarms); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return createAlarm == true;"); + condition.setExpression(expression); + alarmRule.setCondition(condition); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, alarmRule + )); + alarmCf.setConfiguration(configuration); + saveCalculatedField(alarmCf); return deviceProfile; } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index bcbe52b5c9..ace27c08c1 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -595,7 +595,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { for (TopicPartitionInfo partition : expectedPartitions) { if (consumers.stream().noneMatch(consumer -> consumer.subscribed && - consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { + consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { return false; } } @@ -605,7 +605,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { return consumers.size() == 1 && consumers.stream() .anyMatch(consumer -> consumer.subscribed && consumer.pollingStarted && - expectedPartitions.equals(consumer.getPartitions())); + expectedPartitions.equals(consumer.getPartitions())); }); } Mockito.reset(ruleEngineConsumerContext.getSubmitStrategyFactory()); @@ -667,8 +667,8 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().contains(tpi)) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().contains(tpi)) .findFirst().orElse(null), Objects::nonNull); } @@ -676,9 +676,9 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().stream() - .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().stream() + .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) .findFirst().orElse(null), Objects::nonNull); } @@ -778,10 +778,6 @@ public class TbRuleEngineQueueConsumerManagerTest { return false; } - public Set getPartitions() { - return partitions; - } - public void setUpTestMsg() { testMsg = TbMsg.newMsg() .type(TbMsgType.POST_TELEMETRY_REQUEST) @@ -790,6 +786,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .data("{}") .build(); } + } } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java index 1106fad5b6..9bd9bb2e7a 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; @@ -191,6 +192,7 @@ public class TbRuleEngineStrategyTest { queue.setProcessingStrategy(processingStrategy); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + ConsumerKey consumerKey = new ConsumerKey(queueKey, null); var consumerManager = TbRuleEngineQueueConsumerManager.create() .ctx(ruleEngineConsumerContext) .queueKey(queueKey) @@ -238,7 +240,7 @@ public class TbRuleEngineStrategyTest { .map(this::toProto) .toList(); - consumerManager.processMsgs(protoMsgs, consumer, queueKey, queue); + consumerManager.processMsgs(protoMsgs, consumer, consumerKey, queue); processingData.forEach(data -> { verify(actorContext, times(data.attempts)).tell(argThat(msg -> diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java index 72f8cc5b4f..839b8c7cad 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java @@ -64,6 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. "coap.server.enabled=true", "coap.dtls.enabled=true", "coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem", + "coap.dtls.x509.skip_validity_check_for_client_cert=true", "device.connectivity.coaps.enabled=true", "service.integrations.supported=ALL", "transport.coap.enabled=true", diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 226ef0dc57..a7d4cea8d4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; +import org.awaitility.core.ConditionTimeoutException; import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -95,6 +96,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSec; @@ -119,6 +121,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19; @Slf4j @@ -304,7 +307,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte protected final Set expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected ScheduledExecutorService executor; protected LwM2MTestClient lwM2MTestClient; - private String[] resources; + private String[] resources = lwm2mClientResources; protected String deviceId; protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false; @@ -546,7 +549,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } public void setResources(String[] resources) { - this.resources = resources; + if (this.resources == null || !Arrays.equals(this.resources, resources)) { + this.resources = resources; + } } public void createNewClient(Security security, Security securityBs, boolean isRpc, @@ -741,11 +746,19 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte return credentials; } - protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { - await("ObserveReadAll: countObserve " + cntObserve) - .atMost(40, TimeUnit.SECONDS) - .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); + + protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { + try { + await("ObserveReadAll: countObserve " + cntObserve) + .atMost(40, TimeUnit.SECONDS) + .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); + } catch (ConditionTimeoutException e) { + int current = getCntObserveAll(deviceIdStr); + log.error("Condition or device {} with alias 'ObserveReadAll: countObserve {}, but received {}", deviceIdStr, cntObserve, current); + throw e; + } } + protected void awaitDeleteDevice(String deviceIdStr) throws Exception { await("Delete device with id: " + deviceIdStr) .atMost(40, TimeUnit.SECONDS) @@ -756,6 +769,19 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte }); } + protected void updateRegAtLeastOnceAfterAction() { + long initialInvocationCount = countUpdateReg(); + AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); + log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); + await("Update Registration at-least-once after action") + .atMost(50, TimeUnit.SECONDS) + .until(() -> { + newInvocationCount.set(countUpdateReg()); + return newInvocationCount.get() > initialInvocationCount; + }); + log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); + } + protected Integer getCntObserveAll(String deviceIdStr) throws Exception { String actualResult = sendObserveOK("ObserveReadAll", null, deviceIdStr); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index ce073d137d..c5233b378b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -17,7 +17,7 @@ package org.thingsboard.server.transport.lwm2m; public class Lwm2mTestHelper { - public static final String[] lwm2mClientResources = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + public static final String[] lwm2mClientResources = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; // Models public static final int BINARY_APP_DATA_CONTAINER = 19; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java index 051a68c9ab..8e247a8814 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; @@ -32,6 +33,9 @@ import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicInteger; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TITLE; @Slf4j public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { @@ -44,6 +48,12 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); + private LeshanClient leshanClient; + private String pkgNameDef = "firmware"; + private String pkgName; + private String pkgVersionDef = "1.0.0"; + private String pkgVersion; + @Override public ReadResponse read(LwM2mServer identity, int resourceId) { if (!identity.isSystem()) @@ -74,7 +84,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { switch (resourceId) { case 2: - startUpdating(); + startUpdating(identity); return ExecuteResponse.success(); default: return super.execute(identity, resourceId, arguments); @@ -106,11 +116,13 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } private String getPkgName() { - return "firmware"; + this.pkgName = this.pkgName == null ? this.pkgNameDef : this.pkgName; + return this.pkgName; } private String getPkgVersion() { - return "1.0.0"; + this.pkgVersion = this.pkgVersion == null ? this.pkgVersionDef : this.pkgVersion; + return this.pkgVersion; } private int getFirmwareUpdateDeliveryMethod() { @@ -140,7 +152,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { }, 100, TimeUnit.MILLISECONDS); } - private void startUpdating() { + private void startUpdating(LwM2mServer identity) { scheduler.schedule(() -> { try { state.set(3); @@ -148,9 +160,25 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { Thread.sleep(100); updateResult.set(1); fireResourceChange(5); + this.pkgName = TITLE; + fireResourceChange(6); + this.pkgVersion = TARGET_FW_VERSION; + fireResourceChange(7); + if (this.leshanClient != null) { + log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity)); + this.leshanClient.stop(false); + log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity)); + this.leshanClient.start(); + this.pkgName = this.pkgNameDef; + this.pkgVersion = this.pkgVersionDef; + } } catch (Exception e) { } }, 100, TimeUnit.MILLISECONDS); } + protected void setLeshanClient(LeshanClient leshanClient) { + this.leshanClient = leshanClient; + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 993e416ded..d2543f84d7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -467,6 +467,7 @@ public class LwM2MTestClient { this.awaitClientAfterStartConnectLw(); } lwM2mTemperatureSensor12.setLeshanClient(leshanClient); + fwLwM2MDevice.setLeshanClient(leshanClient); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index a67f3cf00f..cff56694e5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -54,7 +54,6 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU @DaoSqlTest public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { - private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml", "19.xml"}; protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; @@ -186,10 +185,6 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg " \"attributeLwm2m\": {}\n" + " }"; - public AbstractOtaLwM2MIntegrationTest() { - setResources(this.RESOURCES_OTA); - } - protected OtaPackageInfo createFirmware(String version, DeviceProfileId deviceProfileId) throws Exception { String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java index 3065255687..e4db6f70bf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java @@ -45,6 +45,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; @@ -91,13 +92,14 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5_Ok() throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration); - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5)); - final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5, deviceProfile.getId()); - createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5, device.getId().getId().toString()); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5 + "Ok", transportConfiguration); + String endpoint = this.CLIENT_ENDPOINT_OTA5 + "Ok"; + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); + final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + createNewClient(SECURITY_NO_SEC, null, false, endpoint, device.getId().getId().toString()); awaitObserveReadAll(5, device.getId().getId().toString()); - device.setFirmwareId(createFirmware("fw.v.1.5.0-update", deviceProfile.getId()).getId()); + device.setFirmwareId(createFirmware(TARGET_FW_VERSION, deviceProfile.getId()).getId()); final Device savedDevice = doPost("/api/device", device, Device.class); assertThat(savedDevice).as("saved device").isNotNull(); @@ -110,7 +112,6 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { log.warn("Object5: Got the ts: {}", ts); } - /** * ObjectId = 19/65533/0 * { @@ -133,13 +134,14 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5WithObject19_Ok() throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration); - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5)); - final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5, deviceProfile.getId()); - createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5, device.getId().getId().toString()); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5 + "19_Ok", transportConfiguration); + String endpoint = this.CLIENT_ENDPOINT_OTA5 + "19_Ok"; + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); + final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + createNewClient(SECURITY_NO_SEC, null, false, endpoint, device.getId().getId().toString()); awaitObserveReadAll(6, device.getId().getId().toString()); - OtaPackageInfo otaPackageInfo = createFirmware("fw.v.1.5.0-update", deviceProfile.getId()); + OtaPackageInfo otaPackageInfo = createFirmware(TARGET_FW_VERSION, deviceProfile.getId()); device.setFirmwareId(otaPackageInfo.getId()); final Device savedDevice = doPost("/api/device", device, Device.class); @@ -154,6 +156,6 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version; String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + FW_INSTANCE_ID + "/" + RESOURCE_ID_0; resultReadOtaParams_19(resourceIdVer, otaPackageInfo); - log.warn("Object5: Got the ts: {}", ts); + log.warn("Object5 with Object19: Got the ts: {}", ts); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java index 7e9efbbf75..f333fd012c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java @@ -20,9 +20,8 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() { - String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; - setResources(RESOURCES_RPC_VER_1_1); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() throws Exception { + String[] RESOURCES_RPC_VER_1_0 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_0); } } - diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java index f21c857e8a..b39b3d1130 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java @@ -20,7 +20,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() { + public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() throws Exception { String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_1.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; setResources(RESOURCES_RPC_VER_1_1); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java index 347d04a900..8c07379004 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java @@ -20,9 +20,9 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() { - String[] RESOURCES_RPC_VER_1_1 = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; - setResources(RESOURCES_RPC_VER_1_1); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() throws Exception { + String[] RESOURCES_RPC_VER_1_2 = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_2); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 5ef2c1ad93..6c75f7161b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.transport.lwm2m.rpc; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.link.LinkParser; import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; import org.junit.Before; @@ -40,6 +42,9 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Predicate; import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; @@ -101,10 +106,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @SpyBean protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; - public AbstractRpcLwM2MIntegrationTest() { - setResources(lwm2mClientResources); - } - @Before public void startInitRPC() throws Exception { if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) { @@ -324,19 +325,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg .count(); } - protected void updateRegAtLeastOnceAfterAction() { - long initialInvocationCount = countUpdateReg(); - AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); - log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); - await("Update Registration at-least-once after action") - .atMost(50, TimeUnit.SECONDS) - .until(() -> { - newInvocationCount.set(countUpdateReg()); - return newInvocationCount.get() > initialInvocationCount; - }); - log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); - } - protected long countSendParametersOnThingsboardTelemetryResource(String rezName) { return Mockito.mockingDetails(lwM2mTransportServerHelperTest) .getInvocations().stream() @@ -350,4 +338,36 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg ) .count(); } + + protected String sendDiscover(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); + } + + protected String sendRpcObserveReadAllWithResult() throws Exception { + ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + return rpcActualResult.get("value").asText(); + } + + protected String sendRpcObserveReadAllWithResult(String params) throws Exception { + sendRpcObserveOk("Observe", params); + ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + return rpcActualResult.get("value").asText(); + } + + protected void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(String expectedIdVer) throws Exception { + String expectedIdObserve = "SingleObservation:/3/0/9"; + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + updateRegAtLeastOnceAfterAction(); + lwM2MTestClient.getLeshanClient().stop(false); + lwM2MTestClient.getLeshanClient().start(); + updateRegAtLeastOnceAfterAction(); + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + String actualIdVer = sendDiscover(objectIdVer_3); + assertTrue(actualIdVer.contains(expectedIdVer)); + String actualAllObserve = sendRpcObserveReadAllWithResult(); + assertTrue(actualAllObserve.contains(expectedIdObserve)); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index dceadfb7b5..77960b1c54 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -192,11 +192,6 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration assertTrue(rpcActualResult.get("error").asText().contains(expected)); } - private String sendDiscover(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); - } - private String convertObjectIdToVerId(String path, String ver) { ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString(); try { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java index bd1216eb06..2a4a401107 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java @@ -166,9 +166,4 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } - - private String sendDiscover(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 483442382d..505cebff69 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -335,12 +335,5 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT sendRpcObserveOk("Observe", expectedId_1); sendRpcObserveOk("Observe", expectedId_2); } - - private String sendRpcObserveReadAllWithResult(String params) throws Exception { - sendRpcObserveOk("Observe", params); - ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - return rpcActualResult.get("value").asText(); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java similarity index 71% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java index 3d3640cb2e..e6f035e5cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java @@ -19,13 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; - import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { +public class RpcLwm2mIntegrationObserveVer10Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { + + public RpcLwm2mIntegrationObserveVer10Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -44,5 +45,21 @@ public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MInt updateRegAtLeastOnceAfterAction(); long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + awaitObserveReadAll(1,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * "3_1.0/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3_1.0" - Discover Object - find ver + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_0() throws Exception { + String expectedIdVer = ";ver=1.0"; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java similarity index 72% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java index a4f7727773..a64ff43b92 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java @@ -23,7 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { +public class RpcLwm2mIntegrationObserveVer11Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { + + public RpcLwm2mIntegrationObserveVer11Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -43,4 +46,19 @@ public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MInt long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); } + + /** + * "3_1.1/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3" - Discover Object - find ver (lwm2mVersion == 1.1) + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_1() throws Exception { + String expectedIdVer = ""; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java similarity index 73% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java index a166e0139b..89062dd303 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java @@ -18,14 +18,16 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test; import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { +public class RpcLwm2mIntegrationObserveVer12Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { + + public RpcLwm2mIntegrationObserveVer12Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -45,4 +47,19 @@ public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MInt long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); } + + /** + * "3_1.2/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3_1.2" - Discover Object - find ver + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_2() throws Exception { + String expectedIdVer = ";ver=1.2"; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index a16b782acf..e642035b32 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -22,6 +22,7 @@ import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.util.Hex; import org.junit.Assert; +import org.junit.Before; import org.springframework.test.web.servlet.MvcResult; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -119,7 +120,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK - private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml", "19.xml"}; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; @@ -134,7 +134,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials - setResources(this.RESOURCES_SECURITY); try { // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); @@ -178,6 +177,12 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M defaultBootstrapCredentials.setLwm2mServer(serverCredentials); } + @Before + public void init() throws Exception { + String[] RESOURCES_SECURITY = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_SECURITY); + } + public void basicTestConnectionStartBS(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 275383104e..a35b663052 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -68,10 +68,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes ON_REGISTRATION_SUCCESS, true); } + @Test public void testWithPskConnectLwm2mOneObserveSuccessUpdateProfileManyObserveUpdateRegistrationSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_PSK; - String identity = CLIENT_PSK_IDENTITY; + String suf = "UpdateReg"; + String clientEndpoint = CLIENT_ENDPOINT_PSK + "_" + suf; + String identity = CLIENT_PSK_IDENTITY + "_" + suf; String keyPsk = CLIENT_PSK_KEY; PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); @@ -103,10 +105,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes awaitObserveReadAll(2, lwm2mDevice.getId().getId().toString()); awaitUpdateReg(3); } + @Test public void testWithPskConnectLwm2mSuccessObserveSuccessUnRegClientUpdateProfileObserveConnectLwm2mSuccessOWithNewObserve() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_PSK; - String identity = CLIENT_PSK_IDENTITY; + String suf = "UnReg"; + String clientEndpoint = CLIENT_ENDPOINT_PSK + "_" + suf; + String identity = CLIENT_PSK_IDENTITY + "_" + suf; String keyPsk = CLIENT_PSK_KEY; PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); @@ -139,7 +143,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Assert.assertNotNull(lwm2mDeviceProfileManyParams); lwM2MTestClient.start(true); - awaitObserveReadAll(2, lwm2mDevice.getId().getId().toString()); + awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString()); awaitUpdateReg(3); } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 2697b2b804..64b2fb032e 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -26,14 +26,18 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.LinkedHashMap; import java.util.List; @@ -43,6 +47,8 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @ExtendWith(MockitoExtension.class) @@ -85,17 +91,23 @@ class CalculatedFieldUtilsTest { geofencingArgumentEntry.setZoneStates(zoneStates); // Create cf state with the geofencing argument and add it to the state map - CalculatedFieldState state = new GeofencingCalculatedFieldState(List.of("geofencingArgumentTest")); - state.updateState(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry)); + CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("geofencingArgumentTest")); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(1); + assertThat(updatedArguments.get("geofencingArgumentTest")).isEqualTo(geofencingArgumentEntry); - // when CalculatedFieldStateProto proto = toProto(stateId, state); + CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(stateId, proto); - // then - CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(proto); assertThat(fromProto) .usingRecursiveComparison() - .ignoringFields("requiredArguments") + .ignoringFields("ctx", "requiredArguments", "readinessStatus") .isEqualTo(state); ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); @@ -106,4 +118,50 @@ class CalculatedFieldUtilsTest { assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getLastPresence()).isNull(); } + @Test + void toProtoAndFromProto_shouldCreatePropagationStateWithoutPropagationArgument() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + AssetId propagationAssetId = new AssetId(UUID.fromString("17bbf99c-3b87-4d21-b07d-da7409bb2bb7")); + PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(List.of(propagationAssetId)); + + long lastUpdateTs = System.currentTimeMillis(); + SingleValueArgumentEntry singleValueArgumentEntry = new SingleValueArgumentEntry(new BaseAttributeKvEntry(new StringDataEntry("state", "active"), lastUpdateTs, 1L)); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("state")); + + CalculatedFieldState state = new PropagationCalculatedFieldState(DEVICE_ID); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry, "state", singleValueArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(2); + assertThat(updatedArguments.get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(updatedArguments.get("state")).isEqualTo(singleValueArgumentEntry); + + // when + CalculatedFieldStateProto proto = toProto(stateId, state); + + // then + CalculatedFieldState restored = CalculatedFieldUtils.fromProto(stateId, proto); + + // Propagation argument is not persisted -> should be absent after restore + assertThat(restored).isNotNull(); + assertThat(restored).isInstanceOf(PropagationCalculatedFieldState.class); + + PropagationCalculatedFieldState propagationState = (PropagationCalculatedFieldState) restored; + + assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); + assertThat(propagationState.getArguments()).isNotNull(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNull(); + assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); + assertThat(propagationState.getRequiredArguments()).isNull(); + assertThat(propagationState.getReadinessStatus()).isNull(); + } + } diff --git a/application/src/test/resources/lwm2m/3.xml b/application/src/test/resources/lwm2m/3-1_2.xml similarity index 100% rename from application/src/test/resources/lwm2m/3.xml rename to application/src/test/resources/lwm2m/3-1_2.xml diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 1805788007..6ce3d5bde9 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.cluster; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TbResourceInfo; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -130,8 +132,14 @@ public interface TbClusterService extends TbQueueClusterService { void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId); + void onCustomerUpdated(Customer customer, Customer oldCustomer); + void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback); void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback); + void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + + void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index f9483965cc..3e1462b445 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -38,6 +38,8 @@ public interface TbQueueConsumer { boolean isStopped(); + Set getPartitions(); + List getFullTopicNames(); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index e26955d465..05abc4b0c7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -51,10 +51,6 @@ import java.util.UUID; public interface AlarmService extends EntityDaoService { - /* - * New API, since 3.5. - */ - /** * Designed for atomic operations over active alarms. * Only one active alarm may exist for the pair {originatorId, alarmType} @@ -74,7 +70,7 @@ public interface AlarmService extends EntityDaoService { AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); - AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 85cd8d24fd..57c6df3c7f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; @@ -35,7 +36,7 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); @@ -45,7 +46,7 @@ public interface CalculatedFieldService extends EntityDaoService { PageData findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java index e9350842f8..dca2e81f29 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -26,6 +27,8 @@ public interface EntityDaoService { Optional> findEntity(TenantId tenantId, EntityId entityId); + FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId); + default long countByTenantId(TenantId tenantId) { throw new IllegalArgumentException("Not implemented for " + getEntityType()); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 9adf703e0b..0e41e0b031 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,6 +39,8 @@ public interface EntityService { Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId); + FluentFuture> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId); + Optional> fetchEntity(TenantId tenantId, EntityId entityId); Map fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set entityIds); @@ -47,4 +50,5 @@ public interface EntityService { long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index b039dbafd1..a1bf54b1ee 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -32,9 +32,6 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; -/** - * Created by Victor Basanets on 8/27/2017. - */ public interface EntityViewService extends EntityDaoService { EntityView saveEntityView(EntityView entityView); @@ -55,6 +52,8 @@ public interface EntityViewService extends EntityDaoService { EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache); + ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId); + EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name); ListenableFuture findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name); @@ -77,8 +76,6 @@ public interface EntityViewService extends EntityDaoService { ListenableFuture> findEntityViewsByQuery(TenantId tenantId, EntityViewSearchQuery query); - ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId); - ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId); List findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId); @@ -98,4 +95,5 @@ public interface EntityViewService extends EntityDaoService { PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index a0bc9a72e6..aaa52f99a7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; +import java.util.function.Predicate; /** * Created by ashvayka on 27.04.17. @@ -86,6 +87,10 @@ public interface RelationService { ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter); + + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 654de3edae..20a09d88ab 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -111,8 +111,9 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); + int countTenantAdmins(TenantId tenantId); + PageData findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink); boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 57a0d24466..e170dbc467 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.DeviceData; 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.EntityId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; @@ -142,6 +143,11 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java index 6a5501840d..d7900e96ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java @@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.id.CustomerId; public interface HasCustomerId { CustomerId getCustomerId(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java index 22934de813..1fb49a46f7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java @@ -34,21 +34,23 @@ public class ProfileEntityIdInfo implements Serializable, HasTenantId { private static final long serialVersionUID = 8532058281983868003L; private final TenantId tenantId; + private final EntityId ownerId; private final EntityId profileId; private final EntityId entityId; - private ProfileEntityIdInfo(UUID tenantId, EntityId profileId, EntityId entityId) { + private ProfileEntityIdInfo(UUID tenantId, EntityId ownerId, EntityId profileId, EntityId entityId) { this.tenantId = TenantId.fromUUID(tenantId); + this.ownerId = ownerId; this.profileId = profileId; this.entityId = entityId; } - public static ProfileEntityIdInfo create(UUID tenantId, DeviceProfileId profileId, DeviceId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, DeviceProfileId profileId, DeviceId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } - public static ProfileEntityIdInfo create(UUID tenantId, AssetProfileId profileId, AssetId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, AssetProfileId profileId, AssetId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index f83a812529..6a475daae3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -40,5 +40,6 @@ public class SystemParams { long maxDataPointsPerRollingArg; int minAllowedScheduledUpdateIntervalInSecForCF; int maxRelationLevelPerCfArgument; + long minAllowedDeduplicationIntervalInSecForCF; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java new file mode 100644 index 0000000000..9a4e875154 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.id.DashboardId; + +@Data +public class AlarmRule { + + @Valid + @NotNull + private AlarmCondition condition; + private String alarmDetails; + private DashboardId dashboardId; + + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return condition.hasSchedule(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java new file mode 100644 index 0000000000..9bb549994b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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 jakarta.validation.Valid; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmCondition.class), + @Type(name = "DURATION", value = DurationAlarmCondition.class), + @Type(name = "REPEATING", value = RepeatingAlarmCondition.class), +}) +@Data +@NoArgsConstructor +public abstract class AlarmCondition { + + @NotNull + @Valid + private AlarmConditionExpression expression; + @Valid + private AlarmConditionValue schedule; + + @JsonIgnore + public boolean hasSchedule() { + return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); + } + + @JsonIgnore + public abstract AlarmConditionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java new file mode 100644 index 0000000000..fd98ed2984 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +public enum AlarmConditionType { + SIMPLE, + DURATION, + REPEATING +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java new file mode 100644 index 0000000000..fab3a78ab3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlarmConditionValue { + + private T staticValue; + private String dynamicValueArgument; + + @JsonIgnore + @AssertTrue(message = "Either staticValue or dynamicValueArgument must be set") + public boolean isValid() { + return staticValue != null ^ dynamicValueArgument != null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java new file mode 100644 index 0000000000..22733ab78d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DurationAlarmCondition extends AlarmCondition { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.DURATION; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java new file mode 100644 index 0000000000..7919a6a22a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RepeatingAlarmCondition extends AlarmCondition { + + @Valid + @NotNull + private AlarmConditionValue count; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.REPEATING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java new file mode 100644 index 0000000000..8e2a7593b0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +public class SimpleAlarmCondition extends AlarmCondition { + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.SIMPLE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java new file mode 100644 index 0000000000..e855f8efd3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmConditionExpression.class), + @Type(name = "TBEL", value = TbelAlarmConditionExpression.class), +}) +public interface AlarmConditionExpression { + + @JsonIgnore + AlarmConditionExpressionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java new file mode 100644 index 0000000000..f0b8f5253d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +public enum AlarmConditionExpressionType { + SIMPLE, + TBEL +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java new file mode 100644 index 0000000000..e99849ea82 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.EntityKeyValueType; + +import java.io.Serializable; +import java.util.List; + +@Data +public class AlarmConditionFilter implements Serializable { + + @NotBlank + private String argument; + @NotNull + private EntityKeyValueType valueType; + private ComplexOperation operation; + @Valid + @NotNull + private List predicates; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java similarity index 82% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java rename to common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java index ba7dc5dce8..21c28fa552 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.profile; - -enum AlarmStateUpdateResult { - - NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; +package org.thingsboard.server.common.data.alarm.rule.condition.expression; +public enum ComplexOperation { + AND, + OR } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java new file mode 100644 index 0000000000..8c27400961 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SimpleAlarmConditionExpression implements AlarmConditionExpression { + + @Valid + @NotEmpty + private List filters; + private ComplexOperation operation; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.SIMPLE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java new file mode 100644 index 0000000000..50f73e887b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class TbelAlarmConditionExpression implements AlarmConditionExpression { + + @NotBlank + private String expression; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.TBEL; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java new file mode 100644 index 0000000000..94dced5fe4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private BooleanOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.BOOLEAN; + } + + public enum BooleanOperation { + EQUAL, + NOT_EQUAL + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java new file mode 100644 index 0000000000..4e24ea28ba --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; + +import java.util.List; + +@Data +public class ComplexFilterPredicate implements KeyFilterPredicate { + + private ComplexOperation operation; + private List predicates; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.COMPLEX; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java new file mode 100644 index 0000000000..af7c45ac5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +public enum FilterPredicateType { + STRING, + NUMERIC, + BOOLEAN, + COMPLEX +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java new file mode 100644 index 0000000000..58355c627d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = StringFilterPredicate.class, name = "STRING"), + @Type(value = NumericFilterPredicate.class, name = "NUMERIC"), + @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), + @Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) +public interface KeyFilterPredicate extends Serializable { + + @JsonIgnore + FilterPredicateType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java new file mode 100644 index 0000000000..65316eda88 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class NumericFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private NumericOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NUMERIC; + } + + public enum NumericOperation { + EQUAL, + NOT_EQUAL, + GREATER, + LESS, + GREATER_OR_EQUAL, + LESS_OR_EQUAL + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java new file mode 100644 index 0000000000..0ea4cbf1eb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + + AlarmConditionValue getValue(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java new file mode 100644 index 0000000000..913c12ca1c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class StringFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private StringOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + private boolean ignoreCase; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.STRING; + } + + public enum StringOperation { + EQUAL, + NOT_EQUAL, + STARTS_WITH, + ENDS_WITH, + CONTAINS, + NOT_CONTAINS, + IN, + NOT_IN + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java new file mode 100644 index 0000000000..e7394c94bd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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 java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), + @Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), + @Type(value = CustomTimeSchedule.class, name = "CUSTOM") +}) +public interface AlarmSchedule extends Serializable { + + @JsonIgnore + AlarmScheduleType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java new file mode 100644 index 0000000000..d18d92834e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +public enum AlarmScheduleType { + ANY_TIME, + SPECIFIC_TIME, + CUSTOM +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java new file mode 100644 index 0000000000..e84f767f5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +public class AnyTimeSchedule implements AlarmSchedule { + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.ANY_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java new file mode 100644 index 0000000000..b084494d28 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeSchedule implements AlarmSchedule { + + private String timezone; + private List items; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.CUSTOM; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java new file mode 100644 index 0000000000..8a2bb97c39 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CustomTimeScheduleItem implements Serializable { + + private boolean enabled; + private int dayOfWeek; + private long startsOn; + private long endsOn; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java new file mode 100644 index 0000000000..7242d2c9cd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.util.Set; + +@Data +public class SpecificTimeSchedule implements AlarmSchedule { + + private String timezone; + private Set daysOfWeek; + private long startsOn; + private long endsOn; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.SPECIFIC_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index e732049118..a34b58a4da 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.asset; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -125,6 +127,11 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index 3b2ddf0627..8c5adefcf8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -18,11 +18,14 @@ package org.thingsboard.server.common.data.cf; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; @@ -37,6 +40,10 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; @Schema @Data @@ -46,6 +53,22 @@ public class CalculatedField extends BaseData implements HasN @Serial private static final long serialVersionUID = 4491966747773381420L; + public static final Map> SUPPORTED_ENTITIES = Map.of( + EntityType.DEVICE, CalculatedFieldType.all, + EntityType.ASSET, CalculatedFieldType.all, + EntityType.DEVICE_PROFILE, CalculatedFieldType.all, + EntityType.ASSET_PROFILE, CalculatedFieldType.all, + EntityType.CUSTOMER, Set.of(CalculatedFieldType.ALARM) + ); + + public static final Set SUPPORTED_REFERENCED_ENTITIES = Collections.unmodifiableSet(EnumSet.of( + EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT + )); + + public static boolean isSupportedRefEntity(EntityId entity) { + return SUPPORTED_REFERENCED_ENTITIES.contains(entity.getEntityType()); + } + private TenantId tenantId; private EntityId entityId; @@ -64,6 +87,8 @@ public class CalculatedField extends BaseData implements HasN @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; @Schema(implementation = SimpleCalculatedFieldConfiguration.class) + @Valid + @NotNull private CalculatedFieldConfiguration configuration; @Getter @Setter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index d4dd2c5812..4463c835db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -15,8 +15,19 @@ */ package org.thingsboard.server.common.data.cf; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + public enum CalculatedFieldType { - SIMPLE, SCRIPT, GEOFENCING + SIMPLE, + SCRIPT, + GEOFENCING, + ALARM, + PROPAGATION, + RELATED_ENTITIES_AGGREGATION; + + public static final Set all = Collections.unmodifiableSet(EnumSet.allOf(CalculatedFieldType.class)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..d36ba33849 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.stream.Stream; + +import static java.util.Map.Entry.comparingByKey; + +@Data +public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { + + private Map arguments; + + @Valid + @NotEmpty + private Map createRules; + @Valid + private AlarmRule clearRule; + + private boolean propagate; + private boolean propagateToOwner; + private boolean propagateToTenant; + private List propagateRelationTypes; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + + @Override + public Output getOutput() { + return null; + } + + @JsonIgnore + @Override + public boolean requiresScheduledReevaluation() { + return getAllRules().anyMatch(entry -> entry.getValue().requiresScheduledReevaluation()); + } + + @JsonIgnore + public Stream> getAllRules() { + Stream> rules = createRules.entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), entry.getValue())); + if (clearRule != null) { + rules = Stream.concat(rules, Stream.of(Pair.of(null, clearRule))); + } + return rules.sorted(comparingByKey(Comparator.nullsLast(Comparator.naturalOrder()))); + } + + public boolean rulesEqual(AlarmCalculatedFieldConfiguration other, BiPredicate equalityCheck) { + List> thisRules = this.getAllRules().toList(); + List> otherRules = other.getAllRules().toList(); + return CollectionsUtil.elementsEqual(thisRules, otherRules, (thisRule, otherRule) -> { + if (!Objects.equals(thisRule.getKey(), otherRule.getKey())) { + return false; + } + return equalityCheck.test(thisRule.getValue(), otherRule.getValue()); + }); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index 52935c3411..8d4a831cf9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -37,4 +37,16 @@ public class Argument { return refDynamicSourceConfiguration != null; } + public boolean hasRelationQuerySource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + public boolean hasOwnerSource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.CURRENT_OWNER; + } + + public boolean hasTsRollingArgument() { + return ArgumentType.TS_ROLLING.equals(refEntityKey.getType()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java index 225278e776..f422869c95 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -15,10 +15,29 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + @Valid + @NotEmpty Map getArguments(); + default List getReferencedEntities() { + if (getArguments() == null) { + return List.of(); + } + return getArguments().values().stream() + .map(Argument::getRefEntityId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index c270874605..6913b1ed63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -16,15 +16,8 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; @Data public abstract class BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { @@ -34,21 +27,17 @@ public abstract class BaseCalculatedFieldConfiguration implements ExpressionBase protected Output output; @Override - public List getReferencedEntities() { - return arguments.values().stream() - .map(Argument::getRefEntityId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + public void validate() { + baseCalculatedFieldRestriction(); + if (arguments.values().stream().anyMatch(Argument::hasRelationQuerySource)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support relation query configuration!"); + } } - @Override - public void validate() { + protected void baseCalculatedFieldRestriction() { if (arguments.containsKey("ctx")) { throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); } - if (arguments.values().stream().anyMatch(Argument::hasDynamicSource)) { - throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support dynamic source configuration!"); - } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java index dc52287f3e..a9d374015b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.cf.configuration; public enum CFArgumentDynamicSourceType { + CURRENT_OWNER, RELATION_PATH_QUERY } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 972a3e0ee9..3df9a32dcc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -18,15 +18,16 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; 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 org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -36,9 +37,12 @@ import java.util.stream.Collectors; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), - @JsonSubTypes.Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING") + @Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldConfiguration.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldConfiguration.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldConfiguration.class, name = "RELATED_ENTITIES_AGGREGATION") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CalculatedFieldConfiguration { @@ -48,11 +52,11 @@ public interface CalculatedFieldConfiguration { Output getOutput(); - void validate(); + default void validate() {} @JsonIgnore default List getReferencedEntities() { - return Collections.emptyList(); + return List.of(); } default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { @@ -70,4 +74,9 @@ public interface CalculatedFieldConfiguration { .collect(Collectors.toList()); } + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index ff746161e9..6a0f0c25ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -26,7 +26,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY") + @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY"), + @JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CfArgumentDynamicSourceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java new file mode 100644 index 0000000000..be9a519f1f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import lombok.Data; + +@Data +public class CurrentOwnerDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.CURRENT_OWNER; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..61d4542eb9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration { + + public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; + + @Valid + @NotNull + private RelationPathLevel relation; + + private boolean applyExpressionToResolvedArguments; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public void validate() { + baseCalculatedFieldRestriction(); + propagationRestriction(); + if (!applyExpressionToResolvedArguments) { + arguments.forEach((name, argument) -> { + if (!currentEntitySource(argument)) { + throw new IllegalArgumentException("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + if (argument.getRefEntityKey() == null) { + throw new IllegalArgumentException("Argument: '" + name + "' doesn't have reference entity key configured!"); + } + if (argument.getRefEntityKey().getType() == ArgumentType.TS_ROLLING) { + throw new IllegalArgumentException("Argument type: 'Time series rolling' detected for argument: '" + name + "'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + }); + } else { + boolean noneMatchCurrentEntitySource = arguments.entrySet() + .stream() + .noneMatch(entry -> currentEntitySource(entry.getValue())); + if (noneMatchCurrentEntitySource) { + throw new IllegalArgumentException("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("Expression must be specified for 'Expression result' propagation mode!"); + } + } + } + + public Argument toPropagationArgument() { + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(relation)); + var propagationArgument = new Argument(); + propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + return propagationArgument; + } + + private void propagationRestriction() { + if (arguments.entrySet().stream().anyMatch(entry -> entry.getKey().equals(PROPAGATION_CONFIG_ARGUMENT))) { + throw new IllegalArgumentException("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + } + + private boolean currentEntitySource(Argument argument) { + return argument.getRefEntityId() == null && argument.getRefDynamicSourceConfiguration() == null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index d0c5786f62..e1e8ca1a9b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import jakarta.validation.constraints.PositiveOrZero; + public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { boolean isScheduledUpdateEnabled(); + @PositiveOrZero int getScheduledUpdateInterval(); void setScheduledUpdateInterval(int interval); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java new file mode 100644 index 0000000000..cd0e3f66d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +public enum AggFunction { + MIN, MAX, SUM, AVG, COUNT, COUNT_UNIQUE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java new file mode 100644 index 0000000000..f6df80952e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggFunctionInput implements AggInput { + + private String function; + + @Override + public String getType() { + return "function"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java new file mode 100644 index 0000000000..06929de81c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AggKeyInput.class, name = "key"), + @JsonSubTypes.Type(value = AggFunctionInput.class, name = "function") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface AggInput { + + @JsonIgnore + String getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java new file mode 100644 index 0000000000..1a00a18a9d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggKeyInput implements AggInput { + + private String key; + + @Override + public String getType() { + return "key"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java new file mode 100644 index 0000000000..ebd612b1e0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AggMetric { + + private AggFunction function; + private String filter; + private AggInput input; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..cf7040c4bb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; + +@Data +public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { + + @NotNull + private RelationPathLevel relation; + private Map arguments; + private long deduplicationIntervalInSec; + @Valid + @NotEmpty + private Map metrics; + private Output output; + private boolean useLatestTs; + + private int scheduledUpdateInterval; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public boolean isScheduledUpdateEnabled() { + return true; + } + + @Override + public void validate() { + relation.validate(); + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(Argument::hasTsRollingArgument)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support TS_ROLLING arguments."); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java index 9ea5c19e8c..ad31293061 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -16,8 +16,8 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; +import jakarta.validation.constraints.NotBlank; import lombok.Data; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -30,18 +30,11 @@ public class EntityCoordinates { public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + @NotBlank private final String latitudeKeyName; + @NotBlank private final String longitudeKeyName; - public void validate() { - if (StringUtils.isBlank(latitudeKeyName)) { - throw new IllegalArgumentException("Entity coordinates latitude key name must be specified!"); - } - if (StringUtils.isBlank(longitudeKeyName)) { - throw new IllegalArgumentException("Entity coordinates longitude key name must be specified!"); - } - } - public Map toArguments() { return Map.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(latitudeKeyName), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index b331abc50b..47d344fe1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -32,7 +34,12 @@ import java.util.Objects; @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { + @Valid + @NotNull private EntityCoordinates entityCoordinates; + + @Valid + @NotNull private Map zoneGroups; private boolean scheduledUpdateEnabled; @@ -53,9 +60,10 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal return args; } + @Override public List getReferencedEntities() { - return zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); + return zoneGroups == null ? List.of() : zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); } @Override @@ -65,13 +73,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public void validate() { - if (entityCoordinates == null) { - throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); - } - entityCoordinates.validate(); - if (zoneGroups == null || zoneGroups.isEmpty()) { - throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); - } zoneGroups.forEach((key, value) -> value.validate(key)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java index 2feb6e49d0..a06cb242cf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.AttributeScope; @@ -36,8 +38,10 @@ public class ZoneGroupConfiguration { private EntityId refEntityId; private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; + @NotBlank private final String perimeterKeyName; + @NotNull private final GeofencingReportStrategy reportStrategy; private final boolean createRelationsWithMatchedZones; @@ -48,13 +52,7 @@ public class ZoneGroupConfiguration { if (EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY.equals(name) || EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY.equals(name)) { throw new IllegalArgumentException("Name '" + name + "' is reserved and cannot be used for zone group!"); } - if (StringUtils.isBlank(perimeterKeyName)) { - throw new IllegalArgumentException("Perimeter key name must be specified for '" + name + "' zone group!"); - } - if (reportStrategy == null) { - throw new IllegalArgumentException("Report strategy must be specified for '" + name + "' zone group!"); - } - if (hasDynamicSource()) { + if (refDynamicSourceConfiguration != null) { refDynamicSourceConfiguration.validate(); } if (!createRelationsWithMatchedZones) { @@ -68,8 +66,12 @@ public class ZoneGroupConfiguration { } } - public boolean hasDynamicSource() { - return refDynamicSourceConfiguration != null; + public boolean hasRelationQuerySource() { + return toArgument().hasRelationQuerySource(); + } + + public boolean hasCurrentOwnerSource() { + return toArgument().hasOwnerSource(); } @JsonIgnore @@ -92,4 +94,5 @@ public class ZoneGroupConfiguration { argument.setRefEntityKey(new ReferencedEntityKey(perimeterKeyName, ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); return argument; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index 07c42eb31b..f840886834 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -26,6 +26,7 @@ import java.util.List; @Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class AlarmCondition implements Serializable { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index 210193fc01..6e7e6ab321 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -26,6 +26,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmConditionFilter implements Serializable { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index d31e6710ef..3c6e5252a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -23,6 +23,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmConditionFilterKey implements Serializable { @Schema(description = "The key type", example = "TIME_SERIES") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java index 9eef80e312..6f451a1abc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionKeyType { ATTRIBUTE, TIME_SERIES, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index 37b2a9d7c5..f3f969f641 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -31,6 +31,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) +@Deprecated public interface AlarmConditionSpec extends Serializable { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java index adef445914..229be24b42 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionSpecType { SIMPLE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index faeecd7eb3..633cde766e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -26,6 +26,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmRule implements Serializable { @Serial diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java index 09e8d3c146..4bfa2ef9f6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -31,6 +31,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) +@Deprecated public interface AlarmSchedule extends Serializable { AlarmScheduleType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java index f50a3b47db..ab06cb9335 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmScheduleType { ANY_TIME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java index 426430481a..87766d685c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile; import org.thingsboard.server.common.data.query.DynamicValue; +@Deprecated public class AnyTimeSchedule implements AlarmSchedule { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java index b372a2fa07..6b07341b30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.DynamicValue; import java.util.List; @Data +@Deprecated public class CustomTimeSchedule implements AlarmSchedule { private String timezone; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index abcbec4e32..b0781e3ad1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -20,6 +20,7 @@ import lombok.Data; import java.io.Serializable; @Data +@Deprecated public class CustomTimeScheduleItem implements Serializable { private boolean enabled; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index fb8488c58e..fcbece4d4b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -28,6 +28,7 @@ import java.util.TreeMap; @Schema @Data +@Deprecated public class DeviceProfileAlarm implements Serializable { @Schema(description = "String value representing the alarm rule id", example = "highTemperatureAlarmID") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java index e114ec1ddc..361ba1e4b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class DurationAlarmConditionSpec implements AlarmConditionSpec { private TimeUnit unit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java index f9e3fd6d05..75c07dbe02 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { private FilterPredicateValue predicate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java index 05c8d0df70..4243946a30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java @@ -20,6 +20,7 @@ import lombok.Data; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class SimpleAlarmConditionSpec implements AlarmConditionSpec { @Override public AlarmConditionSpecType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java index e46d5edbf3..a8b47db1ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.DynamicValue; import java.util.Set; @Data +@Deprecated public class SpecificTimeSchedule implements AlarmSchedule { private String timezone; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java index 0424eabeb6..acc5cf6205 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; -@ToString +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class CalculatedFieldDebugEvent extends Event { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index bd2c73d0d8..6f6d924deb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -55,11 +55,17 @@ public abstract class JobResult implements Serializable { public void processTaskResult(TaskResult taskResult) { if (taskResult.isSuccess()) { - successfulCount++; + if (totalCount == null || successfulCount < totalCount) { + successfulCount++; + } } else if (taskResult.isDiscarded()) { - discardedCount++; + if (totalCount == null || discardedCount < totalCount) { + discardedCount++; + } } else { - failedCount++; + if (totalCount == null || failedCount < totalCount) { + failedCount++; + } if (results.size() < 100) { // preserving only first 100 errors, not reprocessing if there are more failures results.add(taskResult); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index f942bc2196..720dc5b790 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -38,7 +38,10 @@ public enum TbMsgType { ENTITY_UNASSIGNED("Entity Unassigned"), ATTRIBUTES_UPDATED("Attributes Updated"), ATTRIBUTES_DELETED("Attributes Deleted"), - ALARM, + ALARM("Alarm"), + ALARM_CREATED("Alarm Created"), + ALARM_UPDATED("Alarm Updated"), + ALARM_SEVERITY_UPDATED("Alarm Severity Updated"), ALARM_ACK("Alarm Acknowledged"), ALARM_CLEAR("Alarm Cleared"), ALARM_DELETE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index cfcf66b016..00040db7aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -47,11 +47,11 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { private boolean cleared; private CustomerId alarmCustomerId; private DashboardId dashboardId; - private Map info; + private Map details; @Override public Map getTemplateData() { - Map templateData = new HashMap<>(info); + Map templateData = new HashMap<>(details); templateData.put("alarmType", alarmType); templateData.put("action", action); templateData.put("alarmId", alarmId.toString()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java index 5d13db2348..31cab71e0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java @@ -32,7 +32,9 @@ public enum ComponentLifecycleEvent implements Serializable { STOPPED(5), DELETED(6), FAILED(7), - DEACTIVATED(8); + DEACTIVATED(8), + RELATION_UPDATED(9), + RELATION_DELETED(10); @Getter private final int protoNumber; // corresponds to ComponentLifecycleEvent proto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java index c28135204f..c04ca09e79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.relation; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.StringUtils; -public record RelationPathLevel(EntitySearchDirection direction, String relationType) { +public record RelationPathLevel(@NotNull EntitySearchDirection direction, @NotBlank String relationType) { public void validate() { if (direction == null) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index ad73510d9e..6d1dbdcb4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -24,11 +24,15 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; +import java.io.Serial; + @Schema @EqualsAndHashCode(callSuper = true) public class DeviceCredentials extends BaseData implements DeviceCredentialsFilter, HasVersion { + @Serial private static final long serialVersionUID = -7869261127032877765L; + private DeviceId deviceId; private DeviceCredentialsType credentialsType; private String credentialsId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index c6bd9a7f38..0e246f9268 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -172,10 +172,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; - @Schema(example = "3600") + @Schema(example = "60") private int minAllowedScheduledUpdateIntervalInSecForCF = 60; @Schema(example = "10") private int maxRelationLevelPerCfArgument = 10; + @Schema(example = "100") + private int maxRelatedEntitiesToReturnPerCfArgument = 100; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") @@ -184,6 +186,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxStateSizeInKBytes = 32; @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; + @Schema(example = "60") + private long minAllowedDeduplicationIntervalInSecForCF = 60; @Override public long getProfileThreshold(ApiUsageRecordKey key) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index 082be9b71f..0d69db556d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -18,9 +18,11 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiPredicate; import java.util.stream.Collectors; public class CollectionsUtil { @@ -95,6 +97,33 @@ public class CollectionsUtil { return false; } + public static boolean elementsEqual(Iterable iterable1, Iterable iterable2, BiPredicate equalityCheck) { + if (iterable1 instanceof Collection collection1 && iterable2 instanceof Collection collection2) { + if (collection1.size() != collection2.size()) { + return false; + } + } + + Iterator iterator1 = iterable1.iterator(); + Iterator iterator2 = iterable2.iterator(); + while (true) { + if (iterator1.hasNext()) { + if (!iterator2.hasNext()) { + return false; + } + + T o1 = iterator1.next(); + T o2 = iterator2.next(); + if (equalityCheck.test(o1, o2)) { + continue; + } else { + return false; + } + } + return !iterator2.hasNext(); + } + } + public static Set addToSet(Set existing, T value) { if (existing == null || existing.isEmpty()) { return Set.of(value); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java index d1108e9eed..ec99b29fe8 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -29,10 +29,21 @@ public class ArgumentTest { } @Test - void validateShouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { + void validateWhenRelationQuerySourceConfigurationIsNotNull() { var argument = new Argument(); argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isTrue(); + assertThat(argument.hasOwnerSource()).isFalse(); + } + + @Test + void validateWhenCurrentOwnerSourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasOwnerSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isFalse(); } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..9c77f1bd21 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@ExtendWith(MockitoExtension.class) +public class PropagationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBePropagation() { + var cfg = new PropagationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithReferencedEntity() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("bda14084-f40e-4acc-9b85-9d1dd209bb64"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithDynamicReferenceConfiguration() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithDynamicRefEntitySource = new Argument(); + argumentWithDynamicRefEntitySource.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + cfg.setArguments(Map.of("argumentWithDynamicRefEntitySource", argumentWithDynamicRefEntitySource)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationHasNoArgumentsWithCurrentEntitySource() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("3703e895-3f9b-4b75-a715-b68f1ad51944"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + + @Test + void validateShouldThrowWhenUsedReservedPropagationArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of(PROPAGATION_CONFIG_ARGUMENT, new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenUsedReservedCtxArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("ctx", new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name 'ctx' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + Argument argument = new Argument(); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument: 'someArgumentName' doesn't have reference entity key configured!"); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyTypeIsTsRolling() { + var cfg = new PropagationCalculatedFieldConfiguration(); + ReferencedEntityKey referencedEntityKey = new ReferencedEntityKey("someKey", ArgumentType.TS_ROLLING, null); + Argument argument = new Argument(); + argument.setRefEntityKey(referencedEntityKey); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument type: 'Time series rolling' detected for argument: 'someArgumentName'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + + @Test + void validateShouldThrowWhenExpressionIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("someArgumentName", new Argument())); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expression must be specified for 'Expression result' propagation mode!"); + } + + @Test + void validateToPropagationArgumentMethodCallReturnCorrectArgument() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + Argument propagationArgument = cfg.toPropagationArgument(); + assertThat(propagationArgument).isNotNull(); + assertThat(propagationArgument.getRefEntityId()).isNull(); + assertThat(propagationArgument.getRefEntityKey()).isNull(); + assertThat(propagationArgument.getDefaultValue()).isNull(); + assertThat(propagationArgument.getTimeWindow()).isNull(); + assertThat(propagationArgument.getLimit()).isNull(); + + assertThat(propagationArgument.getRefDynamicSourceConfiguration()) + .isNotNull() + .isInstanceOf(RelationPathQueryDynamicSourceConfiguration.class); + var refDynamicSourceConfiguration = (RelationPathQueryDynamicSourceConfiguration) propagationArgument.getRefDynamicSourceConfiguration(); + assertThat(refDynamicSourceConfiguration.getLevels()).isNotEmpty().hasSize(1); + + var relationPathLevel = refDynamicSourceConfiguration.getLevels().get(0); + assertThat(relationPathLevel.direction()).isEqualTo(EntitySearchDirection.TO); + assertThat(relationPathLevel.relationType()).isEqualTo(EntityRelation.CONTAINS_TYPE); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java index 3c0956bd08..15a191be97 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; public class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { @Test - void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported() { + void validateDoesNotThrowAnyExceptionWhenScheduledUpdateIntervalIsGreaterThanMinAllowedIntervalInTenantProfile() { int scheduledUpdateInterval = 60; int minAllowedInterval = scheduledUpdateInterval - 1; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java index c5d627c3e6..a8ee18c7d7 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java @@ -16,47 +16,16 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; public class EntityCoordinatesTest { - @ParameterizedTest - @ValueSource(strings = " ") - @NullAndEmptySource - void validateShouldThrowWhenLatitudeCoordinateIsNullEmptyOrBlank(String latitudeKey) { - var entityCoordinates = new EntityCoordinates(latitudeKey, "longitude"); - assertThatThrownBy(entityCoordinates::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Entity coordinates latitude key name must be specified!"); - } - - @ParameterizedTest - @ValueSource(strings = " ") - @NullAndEmptySource - void validateShouldThrowWhenLongitudeCoordinateIsNullEmptyOrBlank(String longitudeKey) { - var entityCoordinates = new EntityCoordinates("latitude", longitudeKey); - assertThatThrownBy(entityCoordinates::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Entity coordinates longitude key name must be specified!"); - } - - @Test - void validateShouldPassOnMinimalValidConfig() { - var entityCoordinates = new EntityCoordinates("latitude", "longitude"); - assertThatCode(entityCoordinates::validate).doesNotThrowAnyException(); - } - @Test void validateToArgumentsMethodCallWithoutRefEntityId() { var entityCoordinates = new EntityCoordinates("xPos", "yPos"); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index c5fb1f8953..2e9d5e4a88 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -24,16 +24,12 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; @@ -47,28 +43,7 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void validateShouldThrowWhenEntityCoordinatesNull() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setEntityCoordinates(null); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Geofencing calculated field entity coordinates must be specified!"); - } - - @Test - void validateShouldThrowWhenZoneGroupsNull() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); - cfg.setZoneGroups(null); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Geofencing calculated field must contain at least one geofencing zone group defined!"); - } - - @Test - void validateShouldCallValidateOnEntityCoordinatesAndZoneGroups() { + void validateShouldCallValidateOnZoneGroups() { var cfg = new GeofencingCalculatedFieldConfiguration(); EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); cfg.setEntityCoordinates(entityCoordinatesMock); @@ -76,13 +51,11 @@ public class GeofencingCalculatedFieldConfigurationTest { cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfiguration)); cfg.validate(); - - verify(entityCoordinatesMock).validate(); verify(zoneGroupConfiguration).validate("someGroupName"); } @Test - void validateShouldCallValidateOnEntityCoordinatesAndZoneGroupsWithoutAnyExceptions() { + void validateShouldCallValidateOnZoneGroupsWithoutAnyExceptions() { var cfg = new GeofencingCalculatedFieldConfiguration(); EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); cfg.setEntityCoordinates(entityCoordinatesMock); @@ -96,7 +69,6 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThatCode(cfg::validate).doesNotThrowAnyException(); - verify(entityCoordinatesMock).validate(); verify(zoneGroupConfigurationA).validate(zoneGroupAName); verify(zoneGroupConfigurationB).validate(zoneGroupBName); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java index beb4639a31..e354a05af9 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -44,24 +45,6 @@ public class ZoneGroupConfigurationTest { .hasMessage("Name '" + name + "' is reserved and cannot be used for zone group!"); } - @ParameterizedTest - @ValueSource(strings = " ") - @NullAndEmptySource - void validateShouldThrowWhenPerimeterKeyNameIsNullEmptyOrBlank(String perimeterKeyName) { - var zoneGroupConfiguration = new ZoneGroupConfiguration(perimeterKeyName, REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Perimeter key name must be specified for 'allowedZonesGroup' zone group!"); - } - - @Test - void validateShouldThrowWhenReportStrategyIsNull() { - var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", null, false); - assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Report strategy must be specified for 'allowedZonesGroup' zone group!"); - } - @ParameterizedTest @ValueSource(strings = " ") @NullAndEmptySource @@ -98,19 +81,25 @@ public class ZoneGroupConfigurationTest { } @Test - void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { + void whenHasRelationQuerySourceCalled_shouldReturnTrueIfRelationQuerySourceConfigurationIsNotNull() { var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); - assertThat(zoneGroupConfiguration.hasDynamicSource()).isTrue(); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isTrue(); } @Test - void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNull() { + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfRelationQuerySourceConfigurationIsNull() { var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); assertThat(zoneGroupConfiguration.getRefDynamicSourceConfiguration()).isNull(); - assertThat(zoneGroupConfiguration.hasDynamicSource()).isFalse(); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); } + @Test + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfCurrentOwnerSourceConfigured() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); + } @Test void validateToArgumentsMethodCallWithoutRefEntityId() { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 563c6015ef..54238c8751 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; @@ -39,7 +38,6 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; class TbMsgTypeTest { private static final List typesWithNullRuleNodeConnection = List.of( - ALARM, ALARM_DELETE, ENTITY_ASSIGNED_TO_EDGE, ENTITY_UNASSIGNED_FROM_EDGE, diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java new file mode 100644 index 0000000000..b16e2adb85 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; + +@Data +public class CalculatedFieldStatePartitionRestoreMsg implements ToCalculatedFieldSystemMsg { + + private final TopicPartitionInfo partition; + + @Override + public TenantId getTenantId() { + return TenantId.SYS_TENANT_ID; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_STATE_PARTITION_RESTORE_MSG; + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 20043582d7..85ce75c829 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -138,16 +138,24 @@ public enum MsgType { CF_CACHE_INIT_MSG, // Sent to init caches for CF actor; CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state; + CF_STATE_PARTITION_RESTORE_MSG, CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures; CF_ENTITY_LIFECYCLE_MSG, // Sent on CF/Device/Asset create/update/delete; + CF_ENTITY_ACTION_EVENT_MSG, + CF_ALARM_ACTION_MSG, CF_TELEMETRY_MSG, // Sent from queue to actor system; CF_LINKED_TELEMETRY_MSG, // Sent from queue to actor system; /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG; + CF_ENTITY_DELETE_MSG, + + CF_RELATION_ACTION_MSG, + + CF_ARGUMENT_RESET_MSG, // Sent to reset argument; + CF_REEVALUATE_MSG; @Getter private final boolean ignoreOnStart; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java index c05c0f121e..869ad659ac 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java @@ -16,12 +16,7 @@ package org.thingsboard.server.common.msg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; public interface ToCalculatedFieldSystemMsg extends TenantAwareMsg { - default TbCallback getCallback() { - return TbCallback.EMPTY; - } - } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java index 4161940398..54ad749ceb 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java @@ -17,9 +17,14 @@ package org.thingsboard.server.common.msg.aware; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; public interface TenantAwareMsg extends TbActorMsg { TenantId getTenantId(); - + + default TbCallback getCallback() { + return TbCallback.EMPTY; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index d57301fd10..23b9fe08e3 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -46,14 +46,15 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { private final String name; private final EntityId oldProfileId; private final EntityId profileId; + private final boolean ownerChanged; private final JsonNode info; public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) { - this(tenantId, entityId, event, null, null, null, null, null); + this(tenantId, entityId, event, null, null, null, null, false, null); } @Builder - private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, JsonNode info) { + private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, boolean ownerChanged, JsonNode info) { this.tenantId = tenantId; this.entityId = entityId; this.event = event; @@ -61,6 +62,7 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { this.name = name; this.oldProfileId = oldProfileId; this.profileId = profileId; + this.ownerChanged = ownerChanged; this.info = info; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java index efee3dcb97..f647714c63 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java @@ -23,16 +23,15 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import java.io.Serial; import java.util.HashSet; import java.util.List; import java.util.Set; -/** - * @author Andrew Shvayka - */ @Data public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg { + @Serial private static final long serialVersionUID = 2422071590415277039L; private final TenantId tenantId; @@ -56,4 +55,5 @@ public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotifi public MsgType getMsgType() { return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG; } + } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index f5a07cf07e..26a64c7f8a 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -129,6 +129,7 @@ public class ProtoUtils { builder.setOldProfileIdMSB(msg.getOldProfileId().getId().getMostSignificantBits()); builder.setOldProfileIdLSB(msg.getOldProfileId().getId().getLeastSignificantBits()); } + builder.setOwnerChanged(msg.isOwnerChanged()); if (msg.getName() != null) { builder.setName(msg.getName()); } @@ -165,6 +166,7 @@ public class ProtoUtils { var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB()))); } + builder.ownerChanged(proto.getOwnerChanged()); if (proto.hasInfo()) { builder.info(JacksonUtil.toJsonNode(proto.getInfo())); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index a05fdd5d36..bfdce15dbd 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -888,6 +888,7 @@ message SingleValueArgumentProto { string argName = 1; TsValueProto value = 2; int64 version = 3; + EntityIdProto entityId = 4; } message TsDoubleValProto { @@ -921,6 +922,9 @@ message CalculatedFieldStateProto { repeated SingleValueArgumentProto singleValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 4; repeated GeofencingArgumentProto geofencingArguments = 5; + AlarmStateProto alarmState = 6; + int64 lastArgsUpdateTs = 7; + int64 lastMetricsEvalTs = 8; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. @@ -1273,6 +1277,8 @@ enum ComponentLifecycleEvent { DELETED = 6; FAILED = 7; DEACTIVATED = 8; + RELATION_UPDATED = 9; + RELATION_DELETED = 10; } message ComponentLifecycleMsgProto { @@ -1290,6 +1296,7 @@ message ComponentLifecycleMsgProto { int64 profileIdMSB = 11; int64 profileIdLSB = 12; optional string info = 13; + bool ownerChanged = 100; } message EdgeEventMsgProto { @@ -1721,6 +1728,7 @@ message ToEdgeEventNotificationMsg { message ToCalculatedFieldMsg { CalculatedFieldTelemetryMsgProto telemetryMsg = 1; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + EntityActionEventProto eventMsg = 3; } message ToCalculatedFieldNotificationMsg { @@ -1894,3 +1902,22 @@ message JobStatsMsg { message TaskResultProto { string value = 1; } + +message EntityActionEventProto { + EntityIdProto tenantId = 1; + EntityIdProto entityId = 2; + string entity = 3; + string action = 4; +} + +message AlarmStateProto { + repeated AlarmRuleStateProto createRuleStates = 1; + AlarmRuleStateProto clearRuleState = 2; +} + +message AlarmRuleStateProto { + string severity = 1; + int64 eventCount = 2; + int64 firstEventTs = 3; + int64 lastEventTs = 4; +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 7e7de64a5c..04fe2443ef 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -194,6 +194,11 @@ public abstract class AbstractTbQueueConsumerTemplate i abstract protected void doUnsubscribe(); + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { if (partitions == null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index db5bac7170..b300e8c1b2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -25,6 +25,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import java.util.Collection; @@ -218,7 +219,7 @@ public class MainQueueConsumerManager consumer) { + private void consumerLoop(ConsumerKey consumerKey, TbQueueConsumer consumer) { try { while (!stopped && !consumer.isStopped()) { try { @@ -250,7 +251,7 @@ public class MainQueueConsumerManager msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception { + protected void processMsgs(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception { log.trace("Processing {} messages", msgs.size()); msgPackProcessor.process(msgs, consumer, consumerKey, config); log.trace("Processed {} messages", msgs.size()); @@ -273,7 +274,7 @@ public class MainQueueConsumerManager { - void process(List msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception; + void process(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception; } public interface ConsumerWrapper { @@ -285,6 +286,7 @@ public class MainQueueConsumerManager { + private final Map> consumers = new HashMap<>(); @Override @@ -307,8 +309,7 @@ public class MainQueueConsumerManager partitions, Consumer onStop, Function startOffsetProvider) { partitions.forEach(tpi -> { - Integer partitionId = tpi.getPartition().orElse(-1); - String key = queueKey + "-" + partitionId; + ConsumerKey key = new ConsumerKey(queueKey, tpi); Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> { @@ -328,9 +329,11 @@ public class MainQueueConsumerManager> getConsumers() { return consumers.values(); } + } class SingleConsumerWrapper implements ConsumerWrapper { + private TbQueueConsumerTask consumer; @Override @@ -346,7 +349,7 @@ public class MainQueueConsumerManager(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed + consumer = new TbQueueConsumerTask<>(new ConsumerKey(queueKey, null), () -> consumerCreator.apply(config, null), null); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { @@ -361,5 +364,7 @@ public class MainQueueConsumerManager { @Getter - private final Object key; + private final ConsumerKey key; private volatile TbQueueConsumer consumer; private volatile Supplier> consumerSupplier; @Getter @@ -41,7 +41,7 @@ public class TbQueueConsumerTask { @Setter private Future task; - public TbQueueConsumerTask(Object key, Supplier> consumerSupplier, Runnable callback) { + public TbQueueConsumerTask(ConsumerKey key, Supplier> consumerSupplier, Runnable callback) { this.key = key; this.consumer = null; this.consumerSupplier = consumerSupplier; @@ -97,4 +97,18 @@ public class TbQueueConsumerTask { return task != null; } + public record ConsumerKey(Object queueKey, TopicPartitionInfo partition) { + + @Override + public String toString() { + if (partition != null) { + Integer partitionId = partition.getPartition().orElse(-1); + return queueKey + "-" + partitionId; + } else { + return queueKey.toString(); + } + } + + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java index be379fb76d..6bcc87af38 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.queue.common.state; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Collections; +import java.util.Set; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; public class DefaultQueueStateService extends QueueStateService { @@ -26,4 +31,18 @@ public class DefaultQueueStateService partitions, RestoreCallback callback) { + if (callback != null) { + for (TopicPartitionInfo partition : partitions) { + callback.onPartitionRestored(partition); + } + callback.onAllPartitionsRestored(); + } + eventConsumer.addPartitions(partitions); + for (PartitionedQueueConsumerManager consumer : otherConsumers) { + consumer.addPartitions(withTopic(partitions, consumer.getTopic())); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java index 2a38c9a86c..60cfe4c98f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -50,7 +50,7 @@ public class KafkaQueueStateService } @Override - protected void addPartitions(QueueKey queueKey, Set partitions, Runnable whenAllProcessed) { + protected void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) { Map eventsStartOffsets = eventsStartOffsetsProvider != null ? eventsStartOffsetsProvider.get() : null; // remembering the offsets before subscribing to states Set statePartitions = withTopic(partitions, stateConsumer.getTopic()); @@ -61,10 +61,13 @@ public class KafkaQueueStateService try { partitionsInProgress.remove(statePartition); log.info("Finished partition {} (still in progress: {})", statePartition, partitionsInProgress); + if (callback != null) { + callback.onPartitionRestored(statePartition); + } if (partitionsInProgress.isEmpty()) { log.info("All partitions processed"); - if (whenAllProcessed != null) { - whenAllProcessed.run(); + if (callback != null) { + callback.onAllPartitionsRestored(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java index e58d5eb036..e98e8dd7e4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java @@ -49,7 +49,7 @@ public abstract class QueueStateService newPartitions, Runnable whenAllProcessed) { + public void update(QueueKey queueKey, Set newPartitions, RestoreCallback callback) { newPartitions = withTopic(newPartitions, eventConsumer.getTopic()); var writeLock = partitionsLock.writeLock(); writeLock.lock(); @@ -71,23 +71,15 @@ public abstract class QueueStateService partitions, Runnable whenAllProcessed) { - if (whenAllProcessed != null) { - whenAllProcessed.run(); - } - eventConsumer.addPartitions(partitions); - for (PartitionedQueueConsumerManager consumer : otherConsumers) { - consumer.addPartitions(withTopic(partitions, consumer.getTopic())); - } - } + protected abstract void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) ; protected void removePartitions(QueueKey queueKey, Set partitions) { eventConsumer.removePartitions(partitions); @@ -122,4 +114,12 @@ public abstract class QueueStateService implements TbQueueCon return stopped; } + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 072a17835d..39a32310a6 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -264,6 +264,8 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", double.class))); + parserConfig.addImport("roundResult", new MethodStub(TbUtils.class.getMethod("roundResult", + double.class, Integer.class))); parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", @@ -1186,6 +1188,16 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); } + public static Object roundResult(double value, Integer precision) { + if (precision == null) { + return value; + } + if (precision.equals(0)) { + return toInt(value); + } + return toFixed(value, precision); + } + public static boolean isNaN(double value) { return Double.isNaN(value); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java index 73a2183564..4f2719fb75 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java @@ -27,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes({ @JsonSubTypes.Type(value = TbelCfSingleValueArg.class, name = "SINGLE_VALUE"), @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING"), - @JsonSubTypes.Type(value = TbelCfTsGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfPropagationArg.class, name = "PROPAGATION_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfRelatedEntitiesArgumentValue.class, name = "RELATED_ENTITIES_ARGUMENT_VALUE") }) public interface TbelCfArg extends TbelCfObject { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java similarity index 89% rename from common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java rename to common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java index f1e8ec16db..0fa0f4a5bf 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java @@ -20,12 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data -public class TbelCfTsGeofencingArg implements TbelCfArg { +public class TbelCfGeofencingArg implements TbelCfArg { private final Object value; @JsonCreator - public TbelCfTsGeofencingArg(@JsonProperty("value") Object value) { + public TbelCfGeofencingArg(@JsonProperty("value") Object value) { this.value = value; } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java new file mode 100644 index 0000000000..83d7e81a86 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TbelCfPropagationArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfPropagationArg(@JsonProperty("value") Object value) { + this.value = value; + } + + @Override + public String getType() { + return "PROPAGATION_CF_ARGUMENT_VALUE"; + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java new file mode 100644 index 0000000000..02d641d576 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +@Data +public class TbelCfRelatedEntitiesArgumentValue implements TbelCfArg { + + private final Map entityInputs; + + @JsonCreator + public TbelCfRelatedEntitiesArgumentValue(@JsonProperty("entityInputs") Map values) { + this.entityInputs = Collections.unmodifiableMap(values); + } + + @Override + public String getType() { + return "RELATED_ENTITIES_ARGUMENT_VALUE"; + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 4dcbd2d69c..38e69246c5 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1154,6 +1154,13 @@ public class TbUtilsTest { Assertions.assertEquals(28, TbUtils.toInt(28.0)); } + @Test + public void roundResult() { + Assertions.assertEquals(1729.1729, TbUtils.roundResult(doubleVal, null)); + Assertions.assertEquals(1729, TbUtils.roundResult(doubleVal, 0)); + Assertions.assertEquals(1729.17, TbUtils.roundResult(doubleVal, 2)); + } + @Test public void isNaN() { assertFalse(TbUtils.isNaN(doubleVal)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 795f40aa20..789c8b6aac 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.config.CoapConfig; import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.config.DtlsConfig; import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; @@ -196,14 +195,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { root = new CoapResource(""); coapServer.add(root); } - root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE, serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE))); - root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE,serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE))); + root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE)); + root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE)); } return leshanServer; } private void setServerWithCredentials(LeshanServerBuilder builder) { -// private void setServerWithCredentials(LeshanServerBuilder builder) { if (this.config.getSslCredentials() != null) { SslCredentials sslCredentials = this.config.getSslCredentials(); builder.setPublicKey(sslCredentials.getPublicKey()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java index c46b57fb3f..752b888267 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java @@ -34,21 +34,15 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SOFTWARE_UPDATE_COAP_RESOURCE; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.calculateSzx; @Slf4j public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource { - private final ConcurrentMap tokenToObserveRelationMap = new ConcurrentHashMap<>(); private final ConcurrentMap tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>(); private final OtaPackageDataCache otaPackageDataCache; - private final int chunkSize; - private final int maxResourceBodySize; - public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name, int chunkSize, int maxResourceBodySize) { + public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name) { super(name); this.otaPackageDataCache = otaPackageDataCache; - this.chunkSize = chunkSize; - this.maxResourceBodySize = maxResourceBodySize; this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); } @@ -141,29 +135,23 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource { String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 1 ); UUID currentId = UUID.fromString(idStr); - log.info("Start Read ota data (path): [{}]", exchange.getRequestOptions().getUriPath().toString()); Response response = new Response(CoAP.ResponseCode.CONTENT); byte[] otaData = this.getOtaData(currentId); if (otaData != null && otaData.length > 0) { - if (otaData.length <= this.maxResourceBodySize) { - log.info("Read ota data (length): [{}]", otaData.length); - response.setPayload(otaData); - int chunkSize = calculateSzx(this.chunkSize); - if (exchange.getRequestOptions().hasBlock2()) { - chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); - } else if (exchange.getRequestOptions().hasBlock1()) { - chunkSize = exchange.getRequestOptions().getBlock1().getSzx(); - } - log.info("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), otaData.length, chunkSize, false); - boolean lastFlag = otaData.length <= this.chunkSize; - response.getOptions().setBlock2(chunkSize, lastFlag, 0); - response.setType(CoAP.Type.CON); - exchange.respond(response); + log.debug("Read ota data (length): [{}]", otaData.length); + response.setPayload(otaData); + if (exchange.getRequestOptions().getBlock2() != null) { + int szx = exchange.getRequestOptions().getBlock2().getSzx(); + int chunkSize = exchange.getRequestOptions().getBlock2().getSize(); + boolean lastFlag = otaData.length <= chunkSize; + response.getOptions().setBlock2(szx, lastFlag, 0); + log.trace("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], szx [{}], moreFlag [{}]", currentId, otaData.length, chunkSize, szx, lastFlag); } else { - log.info("Ota package size: [{}] is larger than server's MAX_RESOURCE_BODY_SIZE [{}]", otaData.length, this.maxResourceBodySize); + log.trace("With block1 Send currentId: [{}], length: [{}], ", currentId, otaData.length); } + exchange.respond(response); } else { - log.info("Ota packaged currentId: [{}] is not found.", currentId.toString()); + log.trace("Ota packaged currentId: [{}] is not found.", currentId); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 201376dbb7..b7e56c139a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -224,8 +224,11 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl log.info("[{}] Closing old session: {}", registration.getEndpoint(), new UUID(oldSessionInfo.get().getSessionIdMSB(), oldSessionInfo.get().getSessionIdLSB())); sessionManager.deregister(oldSessionInfo.get()); } - logService.log(lwM2MClient, LOG_LWM2M_INFO + ": Client registered with registration id: " + registration.getId() + " version: " - + registration.getLwM2mVersion() + " and modes: " + registration.getQueueMode() + ", " + registration.getBindingMode()); + String msgLogService = String.format(""" + %s: Endpoint [%s] Client registered with registration id: [%s] LwM2mVersion: [%s], SupportedObjectIdVer [%s] QueueMode [%s], BindingMode %s + """, LOG_LWM2M_INFO, registration.getEndpoint(), registration.getId(), registration.getLwM2mVersion(), registration.getSupportedObject(), registration.getQueueMode(), registration.getBindingMode()); + logService.log(lwM2MClient, msgLogService); + log.debug(msgLogService); sessionManager.register(lwM2MClient.getSession()); this.initClientTelemetry(lwM2MClient); this.initAttributes(lwM2MClient, true); @@ -244,7 +247,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to invalid state: " + stateException.getState()); } } catch (Throwable t) { - log.error("[{}] endpoint [{}] error Unable registration.", registration.getEndpoint(), t); + log.error("Endpoint [{}], Error Unable registration: [{}].", registration.getEndpoint(), t.getMessage(), t); logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to: " + t.getMessage()); } }); @@ -290,7 +293,6 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl clientContext.unregister(client, registration); SessionInfoProto sessionInfo = client.getSession(); if (sessionInfo != null) { - securityStore.remove(client.getEndpoint(), client.getRegistration().getId()); sessionManager.deregister(sessionInfo); sessionStore.remove(registration.getEndpoint()); log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index c1fbbcb006..75ac177969 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -121,10 +121,6 @@ public class LwM2MTransportUtil { } } - public static List getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) { - return toLwM2MClientProfile(deviceProfile).getBootstrap(); - } - public static String fromVersionedIdToObjectId(String pathIdVer) { try { if (pathIdVer == null) { @@ -399,13 +395,6 @@ public class LwM2MTransportUtil { } } - public static int calculateSzx(int size) { - if (size < 16 || size > 1024 || (size & (size - 1)) != 0) { - throw new IllegalArgumentException("Size must be a power of 2 between 16 and 1024."); - } - return (int) (Math.log(size / 16) / Math.log(2)); - } - public static ConcurrentHashMap groupByObjectIdVersionedIds(Set targetIds) { return targetIds.stream() .collect(Collectors.groupingBy( diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index a756b84fe8..867b39cd23 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -46,9 +46,6 @@ import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -/** - * Created by valerii.sosliuk on 11/6/16. - */ @Slf4j @Component("MqttSslHandlerProvider") @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 7f9da9e0b9..4397802951 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -133,9 +133,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish; -/** - * @author Andrew Shvayka - */ @Slf4j public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener>, SessionMsgListener { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index c630def89f..5d127fccb7 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -25,9 +25,6 @@ import io.netty.handler.ssl.SslHandler; import org.thingsboard.server.transport.mqtt.limits.IpFilter; import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter; -/** - * @author Andrew Shvayka - */ public class MqttTransportServerInitializer extends ChannelInitializer { private final MqttTransportContext context; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java index c7ff8912aa..d62f35d561 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java @@ -35,9 +35,6 @@ import org.thingsboard.server.common.data.TbTransportService; import java.net.InetSocketAddress; -/** - * @author Andrew Shvayka - */ @Service("MqttTransportService") @TbMqttTransportComponent @Slf4j diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 64f7e5df4a..61a886a7db 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -45,10 +45,6 @@ import java.util.UUID; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; - -/** - * @author Andrew Shvayka - */ @Component @Slf4j public class JsonMqttAdaptor implements MqttTransportAdaptor { @@ -122,7 +118,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); } - + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index 012709c31f..356004246f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -41,9 +41,6 @@ import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionConte import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface MqttTransportAdaptor { ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); @@ -90,4 +87,5 @@ public interface MqttTransportAdaptor { payload.writeBytes(payloadInBytes); return new MqttPublishMessage(mqttFixedHeader, header, payload); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index fd10ad750e..4472bb77d2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -48,7 +48,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMessageDescriptor()); try { return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); } catch (Exception e) { @@ -228,4 +228,5 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { private int getRequestId(String topicName, String topic) { return Integer.parseInt(topicName.substring(topic.length())); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java index 8ce0fc5c1e..5bb3c9c28f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java @@ -33,9 +33,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import java.util.UUID; import java.util.concurrent.ConcurrentMap; -/** - * Created by ashvayka on 19.01.17. - */ @ToString(callSuper = true) @Slf4j public abstract class AbstractGatewayDeviceSessionContext extends MqttDeviceAwareSessionContext implements SessionMsgListener { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index fee2f04e24..6542492238 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -96,9 +96,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; -/** - * Created by ashvayka on 19.01.17. - */ @Slf4j public abstract class AbstractGatewaySessionHandler { @@ -116,6 +113,7 @@ public abstract class AbstractGatewaySessionHandler deviceCreationLockMap; + @Getter private final ConcurrentMap devices; private final ConcurrentMap> deviceFutures; protected final ConcurrentMap mqttQoSMap; @@ -821,11 +819,7 @@ public abstract class AbstractGatewaySessionHandler getDevices() { - return this.devices; - } - - protected TransportServiceCallback getAggregatePubAckCallback( + protected TransportServiceCallback getAggregatePubAckCallback( final ChannelHandlerContext ctx, final int msgId, final String deviceName, @@ -915,4 +909,5 @@ public abstract class AbstractGatewaySessionHandler getDefaultAdaptor(); + case V2_JSON -> context.getJsonMqttAdaptor(); + case V2_PROTO -> context.getProtoMqttAdaptor(); + default -> useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor(); + }; } private MqttTransportAdaptor getDefaultAdaptor() { @@ -269,7 +246,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { } } - public Collection getMsgQueueSnapshot(){ + public Collection getMsgQueueSnapshot() { return Collections.unmodifiableCollection(msgQueue); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java index a48ee50054..24d7888a81 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java @@ -22,9 +22,6 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import java.util.concurrent.ConcurrentMap; -/** - * Created by nickAS21 on 26.12.22 - */ @ToString(callSuper = true) public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 2980b2ad60..487fe1a541 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -28,9 +28,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Optional; import java.util.UUID; -/** - * Created by nickAS21 on 26.12.22 - */ @Slf4j public class GatewaySessionHandler extends AbstractGatewaySessionHandler { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index 9db81a4d8b..24d23d7bdc 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -25,9 +25,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; -/** - * Created by ashvayka on 30.08.18. - */ @ToString(callSuper = true) public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { @@ -47,7 +44,7 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo .stream() .filter(entry -> entry.getKey().matches(topic)) .map(Map.Entry::getValue) - .collect(Collectors.toList()); + .toList(); if (!qosList.isEmpty()) { return MqttQoS.valueOf(qosList.get(0)); } else { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java index 950a1f5bd9..ad3c7b9897 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java @@ -15,26 +15,25 @@ */ package org.thingsboard.server.transport.mqtt.session; +import lombok.Getter; + import java.util.regex.Pattern; public class MqttTopicMatcher { + @Getter private final String topic; private final Pattern topicRegex; public MqttTopicMatcher(String topic) { - if(topic == null){ + if (topic == null) { throw new NullPointerException("topic"); } this.topic = topic; this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); } - public String getTopic() { - return topic; - } - - public boolean matches(String topic){ + public boolean matches(String topic) { return this.topicRegex.matcher(topic).matches(); } @@ -52,4 +51,5 @@ public class MqttTopicMatcher { public int hashCode() { return topic.hashCode(); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index f9aba81c90..9ba4c3f989 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -64,9 +64,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP; -/** - * Created by nickAS21 on 12.12.22 - */ @Slf4j @SpecVersion(spec = "sparkplug", version = "3.0.0") public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java similarity index 87% rename from common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java rename to common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java index b1753e7a17..96b45123a1 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java @@ -15,13 +15,16 @@ */ package org.thingsboard.common.util; +import net.objecthunter.exp4j.Expression; +import net.objecthunter.exp4j.ExpressionBuilder; import net.objecthunter.exp4j.function.Function; import net.objecthunter.exp4j.function.Functions; import java.util.ArrayList; import java.util.List; +import java.util.Set; -public class ExpressionFunctionsUtil { +public class ExpressionUtils { public static final List userDefinedFunctions = new ArrayList<>(); @@ -75,4 +78,13 @@ public class ExpressionFunctionsUtil { userDefinedFunctions.add(Functions.getBuiltinFunction("signum")); } + public static Expression createExpression(String expression, Set variables) { + return new ExpressionBuilder(expression) + .functions(userDefinedFunctions) + .implicitMultiplication(true) + .operator() + .variables(variables) + .build(); + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java index a924b0228e..0d9b8494f6 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java @@ -61,6 +61,37 @@ public class KvUtil { } } + public static Long getLongValue(KvEntry entry) { + switch (entry.getDataType()) { + case LONG -> { + return entry.getLongValue().orElse(null); + } + case DOUBLE -> { + return entry.getDoubleValue().map(Double::longValue).orElse(null); + } + case BOOLEAN -> { + return entry.getBooleanValue().map(b -> b ? 1L : 0L).orElse(null); + } + case STRING -> { + try { + return Long.parseLong(entry.getStrValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + case JSON -> { + try { + return Long.parseLong(entry.getJsonValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + default -> { + return null; + } + } + } + public static Boolean getBoolValue(KvEntry entry) { switch (entry.getDataType()) { case LONG: diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java index 7cdf5b027d..89240647ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java @@ -39,6 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @@ -135,6 +136,12 @@ class AiModelServiceImpl extends CachedVersionedEntityService model); // necessary to cast to HasId } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return findAiModelByTenantIdAndIdAsync(tenantId, new AiModelId(entityId.getId())) + .transform(modelOpt -> modelOpt.map(model -> model), directExecutor()); // necessary to cast to HasId + } + @Override public long countByTenantId(TenantId tenantId) { return aiModelDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 5c3e4f3c79..8e5f50da1d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.alarm; - import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; @@ -81,6 +80,7 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -97,8 +97,8 @@ public class BaseAlarmService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findAlarmByIdAsync(tenantId, new AlarmId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 6bf44e4da1..4ddbabb5b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +50,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AssetProfileDaoService") @@ -323,6 +325,12 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(assetProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 7d8b2e0e8c..97f96be48e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.asset; - +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -64,6 +64,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -95,8 +96,8 @@ public class BaseAssetService extends AbstractCachedEntityService keys = new ArrayList<>(2); keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName())); @@ -531,6 +532,12 @@ public class BaseAssetService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findAssetByIdAsync(tenantId, new AssetId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return assetDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index c0cb886747..8e875bfae2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.dao.cf; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; @@ -33,11 +35,15 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; -import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; +import org.thingsboard.server.dao.service.validator.CalculatedFieldLinkDataValidator; +import java.util.EnumSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -52,13 +58,12 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements private final CalculatedFieldDao calculatedFieldDao; private final CalculatedFieldLinkDao calculatedFieldLinkDao; - private final DataValidator calculatedFieldDataValidator; - private final DataValidator calculatedFieldLinkDataValidator; + private final CalculatedFieldDataValidator calculatedFieldDataValidator; + private final CalculatedFieldLinkDataValidator calculatedFieldLinkDataValidator; @Override public CalculatedField save(CalculatedField calculatedField) { - CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - return doSave(calculatedField, oldCalculatedField); + return save(calculatedField, true); } @Override @@ -85,8 +90,9 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, - "calculated_field_unq_key", "Calculated Field with such name is already in exists!", - "calculated_field_external_id_unq_key", "Calculated Field with such external id already exists!"); + "calculated_field_unq_key", calculatedField.getType() == CalculatedFieldType.ALARM ? + "Alarm rule with such type already exists" : "Calculated field with such name and type already exists", + "calculated_field_external_id_unq_key", "Calculated field with such external id already exists"); throw e; } } @@ -100,10 +106,10 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { - log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + public CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name) { + log.trace("Executing findByEntityIdAndTypeAndName entityId [{}], type [{}], name [{}]", entityId, type, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name); + return calculatedFieldDao.findByEntityIdAndTypeAndName(entityId, type, name); } @Override @@ -136,11 +142,18 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { + public PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { log.trace("Executing findAllByEntityId, entityId [{}], pageLink [{}]", entityId, pageLink); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); validatePageLink(pageLink); - return calculatedFieldDao.findAllByEntityId(tenantId, entityId, pageLink); + Set types; + if (type == null) { + types = EnumSet.allOf(CalculatedFieldType.class); + types.remove(CalculatedFieldType.ALARM); + } else { + types = Set.of(type); + } + return calculatedFieldDao.findByEntityIdAndTypes(tenantId, entityId, types, pageLink); } @Override @@ -234,6 +247,12 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return Optional.ofNullable(findById(tenantId, new CalculatedFieldId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(calculatedFieldDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.CALCULATED_FIELD; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index d5465cb8a1..9e8ee61d4c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Set; public interface CalculatedFieldDao extends Dao { @@ -35,16 +37,16 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink); List removeAllByEntityId(TenantId tenantId, EntityId entityId); - long countCFByEntityId(TenantId tenantId, EntityId entityId); + long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 2ed805627f..057ecdffe9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.customer; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -57,6 +58,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("CustomerDaoService") @@ -173,8 +175,13 @@ public class CustomerServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findCustomerByIdAsync(tenantId, new CustomerId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return customerDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 1b21310237..9d8abe467c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -61,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("DashboardDaoService") @@ -70,6 +72,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb public static final String INCORRECT_DASHBOARD_ID = "Incorrect dashboardId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + @Autowired private DashboardDao dashboardDao; @@ -424,6 +427,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return Optional.ofNullable(findDashboardById(tenantId, new DashboardId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findDashboardByIdAsync(tenantId, new DashboardId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return dashboardDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java index ca265e78b1..33b410bcfe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.device; import lombok.Data; @Data -class DeviceCredentialsEvictEvent { +public class DeviceCredentialsEvictEvent { - private final String newCedentialsId; + private final String newCredentialsId; private final String oldCredentialsId; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 12859129a2..3423a74543 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -47,6 +47,8 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; +import java.util.Objects; + import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -61,8 +63,8 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService JacksonUtil.valueToTree(deviceCredentials.getCredentialsId()); + case X509_CERTIFICATE -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue()); + default -> JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class); + }; } private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { @@ -407,4 +407,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(deviceProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; @@ -432,8 +440,7 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService 1) { return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); } - } catch (CertificateException ignored) { - } + } catch (CertificateException ignored) {} return EncryptionUtil.certTrimNewLines(certificateValue); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index f3f3f21d5f..d7eecf49a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -91,6 +92,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -128,23 +130,21 @@ public class DeviceServiceImpl extends CachedVersionedEntityService INCORRECT_DEVICE_ID + id); - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return cache.get(new DeviceCacheKey(deviceId), - () -> deviceDao.findById(tenantId, deviceId.getId())); - } else { - return cache.get(new DeviceCacheKey(tenantId, deviceId), - () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId())); - } + return findDeviceByIdInternal(tenantId, deviceId); } @Override public ListenableFuture findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceByIdAsync [{}]", deviceId); validateId(deviceId, id -> INCORRECT_DEVICE_ID + id); + return executor.submit(() -> findDeviceByIdInternal(tenantId, deviceId)); + } + + private Device findDeviceByIdInternal(TenantId tenantId, DeviceId deviceId) { if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return deviceDao.findByIdAsync(tenantId, deviceId.getId()); + return cache.get(new DeviceCacheKey(deviceId), () -> deviceDao.findById(tenantId, deviceId.getId())); } else { - return deviceDao.findDeviceByTenantIdAndIdAsync(tenantId, deviceId.getId()); + return cache.get(new DeviceCacheKey(tenantId, deviceId), () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId())); } } @@ -275,8 +275,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService toEvict = new ArrayList<>(3); toEvict.add(new DeviceCacheKey(event.getTenantId(), event.getNewName())); @@ -748,6 +748,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findDeviceByIdAsync(tenantId, new DeviceId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java index c12d4d915e..e4e569c00a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.domain; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -39,17 +40,16 @@ import org.thingsboard.server.dao.service.validator.DomainDataValidator; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Slf4j @Service public class DomainServiceImpl extends AbstractEntityService implements DomainService { - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - @Autowired private OAuth2ClientDao oauth2ClientDao; @Autowired @@ -66,8 +66,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build()); return savedDomain; } catch (Exception e) { - checkConstraintViolation(e, - Map.of("domain_name_key", "Domain with such name and scheme already exists!")); + checkConstraintViolation(e, "domain_name_key", "Domain with such name and scheme already exists!"); throw e; } } @@ -142,6 +141,12 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(domainDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -163,4 +168,5 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe public EntityType getEntityType() { return EntityType.DOMAIN; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..f0658ba658 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.edge; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -83,6 +84,7 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -137,8 +139,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(2); keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName())); @@ -629,6 +631,12 @@ public class EdgeServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 2fca546fc9..b512f353f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -67,15 +68,13 @@ import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_NAME; import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; import static org.thingsboard.server.dao.service.Validator.validateId; -/** - * Created by ashvayka on 04.05.17. - */ @Service @Slf4j public class BaseEntityService extends AbstractEntityService implements EntityService { @@ -93,7 +92,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired @Lazy - EntityServiceRegistry entityServiceRegistry; + private EntityServiceRegistry entityServiceRegistry; @Autowired private EdqsService edqsService; @@ -198,6 +197,11 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return fetchAndConvert(tenantId, entityId, this::getCustomerId); } + @Override + public FluentFuture> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId) { + return fetchAndConvertAsync(tenantId, entityId, this::getCustomerId); + } + @Override public Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId); @@ -239,6 +243,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return entityOpt.map(converter); } + private FluentFuture> fetchAndConvertAsync(TenantId tenantId, EntityId entityId, Function, T> converter) { + EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); + return entityDaoService.findEntityAsync(tenantId, entityId) + .transform(entityOpt -> entityOpt.map(converter), directExecutor()); + } + private String getName(HasId entity) { return entity instanceof HasName ? ((HasName) entity).getName() : null; } @@ -329,7 +339,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && - (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { + (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { return false; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 7af9ad8429..617782cb97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.entityview; import com.google.common.base.Function; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -62,13 +63,11 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; -/** - * Created by Victor Basanets on 8/28/2017. - */ @Service("EntityViewDaoService") @Slf4j public class EntityViewServiceImpl extends CachedVersionedEntityService implements EntityViewService { @@ -188,6 +187,17 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService INCORRECT_ENTITY_VIEW_ID + id); + return findEntityViewByIdInternal(tenantId, entityViewId, putInCache); + } + + @Override + public ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) { + log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId); + validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); + return service.submit(() -> findEntityViewByIdInternal(tenantId, entityViewId, true)); + } + + private EntityView findEntityViewByIdInternal(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) { EntityViewCacheValue value = cache.get(EntityViewCacheKey.byId(entityViewId), () -> { EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId()); return new EntityViewCacheValue(entityView, null); @@ -202,7 +212,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null) , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); - } @Override @@ -319,13 +328,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) { - log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId); - validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); - return entityViewDao.findByIdAsync(tenantId, entityViewId.getId()); - } - @Override public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) { log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId); @@ -491,6 +493,12 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findEntityViewByIdAsync(tenantId, new EntityViewId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; 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 360aa0063b..06f219ca3d 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.job; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -44,6 +45,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED; import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED; import static org.thingsboard.server.common.data.job.JobStatus.FAILED; @@ -87,11 +89,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } job.getResult().setCancellationTs(System.currentTimeMillis()); JobStatus prevStatus = job.getStatus(); - if (job.getStatus() == QUEUED) { - job.setStatus(CANCELLED); // setting cancelled status right away, because we don't expect stats for cancelled tasks - } else if (job.getStatus() == PENDING) { - job.setStatus(RUNNING); - } + job.setStatus(CANCELLED); saveJob(tenantId, job, true, prevStatus); } @@ -145,7 +143,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } } - if (job.getStatus() == RUNNING) { + if (job.getStatus().isOneOf(RUNNING, CANCELLED)) { if (result.getTotalCount() != null && result.getCompletedCount() >= result.getTotalCount()) { if (result.getCancellationTs() > 0) { job.setStatus(CANCELLED); @@ -193,7 +191,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi private void checkWaitingJobs(TenantId tenantId, JobType jobType) { Job queuedJob = jobDao.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId, jobType, QUEUED); - if (queuedJob == null) { + if (queuedJob == null || jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, jobType, PENDING, RUNNING)) { return; } queuedJob.setStatus(PENDING); @@ -245,6 +243,12 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return Optional.ofNullable(findJobById(tenantId, (JobId) entityId)); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(jobDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { jobDao.removeById(tenantId, id.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index d20623ad66..98f4a612e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.mobile; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,11 +41,11 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.checkNotNull; @Slf4j @@ -60,7 +61,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Autowired private DataValidator mobileAppBundleDataValidator; - @Override public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) { log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle); @@ -71,8 +71,8 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements return savedMobileApp; } catch (Exception e) { checkConstraintViolation(e, - Map.of("mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!", - "mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!")); + "mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!", + "mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!"); throw e; } } @@ -149,6 +149,12 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(mobileAppBundleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -167,4 +173,5 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements .collect(Collectors.toList()); mobileAppBundleInfo.setOauth2ClientInfos(clients); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index f4e803b28e..b7785496d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.mobile; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -35,9 +36,10 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; -import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Slf4j @Service public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { @@ -58,8 +60,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); return savedMobileApp; } catch (Exception e) { - checkConstraintViolation(e, - Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!")); + checkConstraintViolation(e, "mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!"); throw e; } } @@ -88,6 +89,12 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(mobileAppDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -117,4 +124,5 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil public EntityType getEntityType() { return EntityType.MOBILE_APP; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java index ffed043fe2..35c7839429 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -38,6 +39,8 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j @RequiredArgsConstructor @@ -129,13 +132,17 @@ public class DefaultNotificationRequestService implements NotificationRequestSer return Optional.ofNullable(findNotificationRequestById(tenantId, new NotificationRequestId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationRequestDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; } - private static class NotificationRequestValidator extends DataValidator { - - } + private static class NotificationRequestValidator extends DataValidator {} } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java index c349b84f0a..b2f79c9555 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -33,9 +34,10 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import java.util.List; -import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @RequiredArgsConstructor public class DefaultNotificationRuleService extends AbstractEntityService implements NotificationRuleService, EntityDaoService { @@ -56,9 +58,7 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem .created(notificationRule.getId() == null).build()); return savedRule; } catch (Exception e) { - checkConstraintViolation(e, Map.of( - "uq_notification_rule_name", "Notification rule with such name already exists" - )); + checkConstraintViolation(e, "uq_notification_rule_name", "Notification rule with such name already exists"); throw e; } } @@ -114,6 +114,12 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationRuleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java index 99efd18aee..8c5a051771 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -37,6 +38,8 @@ import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import java.util.Optional; import java.util.Set; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j @RequiredArgsConstructor @@ -95,6 +98,12 @@ public class DefaultNotificationService implements NotificationService, EntityDa return Optional.ofNullable(findNotificationById(tenantId, new NotificationId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index 1c947552ce..637067d51e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -43,10 +44,11 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.user.UserService; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j @RequiredArgsConstructor @@ -65,9 +67,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl .created(notificationTarget.getId() == null).build()); return savedTarget; } catch (Exception e) { - checkConstraintViolation(e, Map.of( - "uq_notification_target_name", "Recipients group with such name already exists" - )); + checkConstraintViolation(e, "uq_notification_target_name", "Recipients group with such name already exists"); throw e; } } @@ -180,6 +180,12 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationTargetDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 8e98714b85..4218e06881 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -37,6 +38,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @RequiredArgsConstructor public class DefaultNotificationTemplateService extends AbstractEntityService implements NotificationTemplateService, EntityDaoService { @@ -144,6 +147,12 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationTemplateDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index 861633ec3b..a373d36580 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.oauth2; +import com.google.common.util.concurrent.FluentFuture; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +42,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @Slf4j @Service("OAuth2ClientService") @@ -107,7 +109,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA .tenantId(tenantId) .entityId(oAuth2ClientId) .build()); - } @Override @@ -149,6 +150,12 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA return Optional.ofNullable(findOAuth2ClientById(tenantId, new OAuth2ClientId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(oauth2ClientDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 343a2485ce..16894fe56c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.ota; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,6 +48,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.nio.ByteBuffer; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -254,6 +256,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index d36d5f2a15..248326a722 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.queue; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -42,6 +43,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.service.Validator.validateId; + @Service("QueueDaoService") @Slf4j @RequiredArgsConstructor @@ -127,7 +131,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Override public void deleteQueuesByTenantId(TenantId tenantId) { - Validator.validateId(tenantId, "Incorrect tenant id for delete queues request."); + validateId(tenantId, __ -> "Incorrect tenant id for delete queues request."); tenantQueuesRemover.removeEntities(tenantId, tenantId); } @@ -141,24 +145,30 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return Optional.ofNullable(findQueueById(tenantId, new QueueId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(queueDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE; } - private PaginatedRemover tenantQueuesRemover = - new PaginatedRemover<>() { + private final PaginatedRemover tenantQueuesRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return queueDao.findQueuesByTenantId(id, pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return queueDao.findQueuesByTenantId(id, pageLink); + } - @Override - protected void removeEntity(TenantId tenantId, Queue entity) { - deleteQueue(tenantId, entity.getId()); - } - }; + @Override + protected void removeEntity(TenantId tenantId, Queue entity) { + deleteQueue(tenantId, entity.getId()); + } + + }; private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) { if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { @@ -167,7 +177,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return tenantId; } } - return TenantId.SYS_TENANT_ID; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java index 9e4c7136d5..4091daac20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.queue; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,6 +36,7 @@ import org.thingsboard.server.dao.service.Validator; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -106,6 +108,12 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(queueStatsDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 18d1806fe4..ffb800c7f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -50,12 +50,14 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.ArrayList; import java.util.Collections; @@ -69,8 +71,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Predicate; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber; /** * Created by ashvayka on 28.04.17. @@ -85,6 +89,8 @@ public class BaseRelationService implements RelationService { private final ApplicationEventPublisher eventPublisher; private final JpaExecutorService executor; private final JpaRelationQueryExecutorService relationsExecutor; + private final ApiLimitService apiLimitService; + protected ScheduledExecutorService timeoutExecutorService; @Value("${sql.relations.query_timeout:20}") @@ -93,13 +99,14 @@ public class BaseRelationService implements RelationService { public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService, TbTransactionalCache cache, ApplicationEventPublisher eventPublisher, JpaExecutorService executor, - JpaRelationQueryExecutorService relationsExecutor) { + JpaRelationQueryExecutorService relationsExecutor, ApiLimitService apiLimitService) { this.relationDao = relationDao; this.entityService = entityService; this.cache = cache; this.eventPublisher = eventPublisher; this.executor = executor; this.relationsExecutor = relationsExecutor; + this.apiLimitService = apiLimitService; } @PostConstruct @@ -501,17 +508,57 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + return findFilteredRelationsByPathQueryAsync(tenantId, relationPathQuery, null); + } + + @Override + public ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter) { log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); validateId(tenantId, id -> "Invalid tenant id: " + id); validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + validatePositiveNumber(limit, "Max related entities limit for relation path query must be positive!"); if (relationPathQuery.levels().size() == 1) { RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); - return switch (relationPathLevel.direction()) { + var relationsFuture = switch (relationPathLevel.direction()) { case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); }; + return Futures.transform(relationsFuture, entityRelations -> { + if (entityRelations == null || entityRelations.isEmpty()) { + return Collections.emptyList(); + } + List relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + return relations.size() > limit ? relations.subList(0, limit) : relations; + }, MoreExecutors.directExecutor()); + } + return executor.submit(() -> { + List entityRelations = relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); + return relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + }); + } + + private List filterRelations(List entityRelations, Predicate relationFilter) { + return entityRelations.stream() + .filter(relationFilter) + .toList(); + } + + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + var relations = switch (relationPathLevel.direction()) { + case FROM -> findByFromAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + return relations.size() > limit ? relations.subList(0, limit) : relations; } - return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery)); + return relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); } private void validate(EntityRelationPathQuery relationPathQuery) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index ad53164ad7..2ec23a0d74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -72,6 +72,6 @@ public interface RelationDao { List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); - List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index c16e37d30f..25ef83d556 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -17,11 +17,11 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; import jakarta.annotation.PostConstruct; -import lombok.Data; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; @@ -356,8 +356,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic imageName = imageName + type + " image"; UpdateResult result = convertToImageUrl(entity.getTenantId(), imageName, entity.getImage(), Collections.emptyMap()); - entity.setImage(result.getValue()); - return result.isUpdated(); + entity.setImage(result.value()); + return result.updated(); } @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure @@ -373,8 +373,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic Map imagesLinks = getResourcesLinks(widgetTypeDetails.getResources()); UpdateResult result = convertToImageUrl(tenantId, prefix + " image", widgetTypeDetails.getImage(), imagesLinks); - boolean updated = result.isUpdated(); - widgetTypeDetails.setImage(result.getValue()); + boolean updated = result.updated(); + widgetTypeDetails.setImage(result.value()); if (widgetTypeDetails.getDescriptor().isObject()) { JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); @@ -397,8 +397,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic Map imagesLinks = getResourcesLinks(dashboard.getResources()); var result = convertToImageUrl(tenantId, prefix + " image", dashboard.getImage(), imagesLinks); - boolean updated = result.isUpdated(); - dashboard.setImage(result.getValue()); + boolean updated = result.updated(); + dashboard.setImage(result.value()); updated |= convertToImageUrlsByMapping(tenantId, DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), dashboard.getConfiguration(), imagesLinks); updated |= convertToImageUrls(tenantId, prefix, dashboard.getConfiguration(), imagesLinks); @@ -409,10 +409,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic AtomicBoolean updated = new AtomicBoolean(false); JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> { UpdateResult result = convertToImageUrl(tenantId, name, value, links); - if (result.isUpdated()) { + if (result.updated()) { updated.set(true); } - return result.getValue(); + return result.value(); }); return updated.get(); } @@ -514,10 +514,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic AtomicBoolean updated = new AtomicBoolean(false); JacksonUtil.replaceAll(root, title, (path, value) -> { UpdateResult result = convertToImageUrl(tenantId, path, value, true, links); - if (result.isUpdated()) { + if (result.updated()) { updated.set(true); } - return result.getValue(); + return result.value(); }); return updated.get(); } @@ -678,16 +678,18 @@ public class BaseImageService extends BaseResourceService implements ImageServic private String getImageLink(String value) { if (value.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images")) { - return StringUtils.removeStart(value, DataConstants.TB_IMAGE_PREFIX); + return Strings.CS.removeStart(value, DataConstants.TB_IMAGE_PREFIX); } else { return null; } } - @Data(staticConstructor = "of") - private static class UpdateResult { - private final boolean updated; - private final String value; + private record UpdateResult(boolean updated, String value) { + + static UpdateResult of(boolean updated, String value) { + return new UpdateResult(updated, value); + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 7f0dd4a6bf..1eed9a6b59 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -79,6 +80,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -107,7 +109,8 @@ public class BaseResourceService extends AbstractCachedEntityService DASHBOARD_RESOURCES_MAPPING = Map.of( @@ -275,7 +278,7 @@ public class BaseResourceService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findResourceInfoByIdAsync(tenantId, new TbResourceId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 15b9717f47..f06e2e45df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.rpc; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -40,6 +42,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseRpcService implements RpcService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_RPC_ID = "Incorrect rpcId "; @@ -113,21 +116,29 @@ public class BaseRpcService implements RpcService { return Optional.ofNullable(findById(tenantId, new RpcId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findRpcByIdAsync(tenantId, new RpcId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.RPC; } - private PaginatedRemover tenantRpcRemover = - new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return rpcDao.findAllRpcByTenantId(id, pageLink); - } - - @Override - protected void removeEntity(TenantId tenantId, Rpc entity) { - deleteRpc(tenantId, entity.getId()); - } - }; + private final PaginatedRemover tenantRpcRemover = new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return rpcDao.findAllRpcByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Rpc entity) { + deleteRpc(tenantId, entity.getId()); + } + + }; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8538bd9492..8912e47c5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.rule; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -28,6 +29,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; @@ -80,6 +83,7 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.DataConstants.TENANT; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -87,9 +91,6 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber; import static org.thingsboard.server.dao.service.Validator.validateString; -/** - * Created by igor on 3/12/18. - */ @Service("RuleChainDaoService") @Slf4j public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { @@ -98,6 +99,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String TB_RULE_CHAIN_INPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainInputNode"; + @Autowired private RuleChainDao ruleChainDao; @@ -260,7 +262,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId(); } if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) - || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { + || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); } if (ruleChainMetaData.getConnections() != null) { @@ -876,6 +878,17 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return Optional.ofNullable(hasId); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + ListenableFuture> future; + if (entityId.getEntityType() == EntityType.RULE_NODE) { + future = findRuleNodeByIdAsync(tenantId, new RuleNodeId(entityId.getId())); + } else { + future = findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId())); + } + return FluentFuture.from(future).transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return ruleChainDao.countByTenantId(tenantId); @@ -919,18 +932,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(), "org.thingsboard.rule.engine.api.RuleNode", "clusteringMode"); - switch (nodeConfigType) { - case ENABLED: - singletonMode = false; - break; - case SINGLETON: - singletonMode = true; - break; - case USER_PREFERENCE: - default: - singletonMode = ruleNode.isSingletonMode(); - break; - } + singletonMode = switch (nodeConfigType) { + case ENABLED -> false; + case SINGLETON -> true; + default -> ruleNode.isSingletonMode(); + }; } catch (Exception e) { log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e)); singletonMode = false; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 67cf32191b..39faf1bc30 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,9 +18,11 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; @@ -46,15 +48,19 @@ public class CalculatedFieldDataValidator extends DataValidator validateCalculatedFieldConfiguration(calculatedField); validateSchedulingConfiguration(tenantId, calculatedField); validateRelationQuerySourceArguments(tenantId, calculatedField); + validateAggregationConfiguration(tenantId, calculatedField); } @Override protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { + if (calculatedField.getType() == CalculatedFieldType.ALARM) { + return; + } long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); if (maxCFsPerEntity <= 0) { return; } - if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) { + if (calculatedFieldDao.countByEntityIdAndTypeNot(tenantId, calculatedField.getEntityId(), CalculatedFieldType.ALARM) >= maxCFsPerEntity) { throw new DataValidationException("Calculated fields per entity limit reached!"); } } @@ -87,7 +93,7 @@ public class CalculatedFieldDataValidator extends DataValidator private void validateSchedulingConfiguration(TenantId tenantId, CalculatedField calculatedField) { if (!(calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledUpdateCfg) - || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { + || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { return; } long minAllowedScheduledUpdateInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF); @@ -100,7 +106,7 @@ public class CalculatedFieldDataValidator extends DataValidator } Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() .stream() - .filter(entry -> entry.getValue().hasDynamicSource()) + .filter(entry -> entry.getValue().hasRelationQuerySource()) .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); if (relationQueryBasedArguments.isEmpty()) { return; @@ -110,6 +116,17 @@ public class CalculatedFieldDataValidator extends DataValidator wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); } + private void validateAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfiguration)) { + return; + } + long minAllowedDeduplicationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedDeduplicationIntervalInSecForCF); + if (aggConfiguration.getDeduplicationIntervalInSec() < minAllowedDeduplicationInterval) { + throw new IllegalArgumentException("Deduplication interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedDeduplicationInterval); + } + } + private static void wrapAsDataValidation(Runnable validation) { try { validation.run(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index 604a37f4bb..7440481700 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.settings; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -31,6 +32,8 @@ import org.thingsboard.server.dao.service.Validator; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -106,6 +109,12 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId())); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(adminSettingsDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ADMIN_SETTINGS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 7755ef036b..6725387fba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -29,7 +29,7 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); @@ -38,14 +38,14 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantId(UUID tenantId, Pageable pageable); @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + - "AND cf.entityId = :entityId " + - "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") - Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, String textSearch, Pageable pageable); + "AND cf.entityId = :entityId AND cf.type IN :types " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List types, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - long countByTenantIdAndEntityId(UUID tenantId, UUID entityId); + long countByTenantIdAndEntityIdAndTypeNot(UUID tenantId, UUID entityId, String type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 385839dded..3cd9285d78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,6 +35,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.Set; import java.util.UUID; @Slf4j @@ -66,8 +68,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { - log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink); - return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + public PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink) { + log.debug("Try to find calculated fields by entityId [{}] and type [{}] and pageLink [{}]", entityId, types, pageLink); + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndEntityIdAndTypes(tenantId.getId(), entityId.getId(), + types.stream().map(Enum::name).toList(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override @@ -95,8 +98,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; - return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> { - AssetId id = new AssetId((UUID) row.get("id")); - AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; + return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); - return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> { - AssetId id = new AssetId((UUID) row.get("id")); - AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } + + private static ProfileEntityIdInfo toInfo(Map row) { + var tenantIdObj = row.get("tenantId"); + UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); + AssetId id = new AssetId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); + AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java index 78ee2795b0..49062f829f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java @@ -22,11 +22,14 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +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.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import java.util.Map; import java.util.UUID; @Repository @@ -52,24 +55,24 @@ public class DefaultNativeDeviceRepository extends AbstractNativeRepository impl @Override public PageData findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; - return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> { - DeviceId id = new DeviceId((UUID) row.get("id")); - DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; + return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); - return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> { - DeviceId id = new DeviceId((UUID) row.get("id")); - DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); + } + + private static ProfileEntityIdInfo toInfo(Map row) { + var tenantIdObj = row.get("tenantId"); + UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); + DeviceId id = new DeviceId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); + DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index b2871313ed..3ab382d2a4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -299,13 +299,16 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery query) { + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery query, int limit) { List levels = query.levels(); if (levels == null || levels.isEmpty()) { - return Collections.emptyList(); + return List.of(); + } + if (limit <= 0) { + return List.of(); } String sql = buildRelationPathSql(query); - Object[] params = buildRelationPathParams(query); + Object[] params = buildRelationPathParams(query, limit); log.trace("[{}] relation path query: {}", tenantId, sql); @@ -330,7 +333,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple .collect(Collectors.toList()); } - private Object[] buildRelationPathParams(EntityRelationPathQuery query) { + private Object[] buildRelationPathParams(EntityRelationPathQuery query, int limit) { final List params = new ArrayList<>(); // seed params.add(query.rootEntityId().getId()); @@ -340,6 +343,10 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple for (var lvl : query.levels()) { params.add(lvl.relationType()); } + + // limit + params.add(limit); + return params.toArray(); } @@ -387,7 +394,8 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple .append("FROM ").append(RELATION_TABLE_NAME).append(" r\n") .append("JOIN ").append(prevForLast).append(" p ON ").append(lastJoin).append("\n") .append("WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") - .append(" AND r.relation_type = ?"); + .append(" AND r.relation_type = ?\n") + .append("LIMIT ?"); return sb.toString(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index b4e8a21372..4b879f9d95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.relation; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -96,4 +95,5 @@ public interface RelationRepository @Param("toId") UUID toId, @Param("toType") String toType, @Param("batchSize") int batchSize); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 35d15bab51..753955089c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -136,6 +136,11 @@ public class JpaUserDao extends JpaAbstractDao implements User DaoUtil.toPageable(pageLink))); } + @Override + public int countTenantAdmins(UUID tenantId) { + return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 0a30a859c6..2254377af3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -78,4 +78,6 @@ public interface UserRepository extends JpaRepository { "u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " + "FROM UserEntity u WHERE u.id > :id ORDER BY u.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + int countByTenantIdAndAuthority(UUID tenantId, Authority authority); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index e165d6cfe6..b9172306bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -44,6 +45,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -230,23 +232,29 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(tenantProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; } - private final PaginatedRemover tenantProfilesRemover = - new PaginatedRemover<>() { + private final PaginatedRemover tenantProfilesRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { - return tenantProfileDao.findTenantProfiles(tenantId, pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, TenantProfile entity) { + removeTenantProfile(tenantId, entity, entity.isDefault()); + } - @Override - protected void removeEntity(TenantId tenantId, TenantProfile entity) { - removeTenantProfile(tenantId, entity, entity.isDefault()); - } - }; + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 5c69680d16..b6c5762d60 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +52,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TenantDaoService") @@ -86,8 +88,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService existsTenantCache; - @TransactionalEventListener(classes = TenantEvictEvent.class) @Override + @TransactionalEventListener public void handleEvictEvent(TenantEvictEvent event) { TenantId tenantId = event.getTenantId(); cache.evict(tenantId); @@ -246,6 +248,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findTenantByIdAsync(tenantId, TenantId.fromUUID(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 1282493650..49ee56e7be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.usagerecord; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -48,11 +49,13 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("ApiUsageStateDaoService") @Slf4j public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private final ApiUsageStateDao apiUsageStateDao; @@ -161,7 +164,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A public ApiUsageState update(ApiUsageState apiUsageState) { log.trace("Executing save [{}]", apiUsageState.getTenantId()); validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id); - validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); + validateId(apiUsageState.getId(), __ -> "Can't save new usage state. Only update is allowed!"); apiUsageState.setVersion(null); ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId()) @@ -195,6 +198,12 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A return Optional.ofNullable(findApiUsageStateById(tenantId, new ApiUsageStateId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(apiUsageStateDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Transactional @Override public void deleteByTenantId(TenantId tenantId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index b60b263ac8..127aa6141a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -101,4 +101,5 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); + int countTenantAdmins(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 4a83c71d66..9209b69ccf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.user; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -79,6 +80,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -112,8 +114,8 @@ public class UserServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(2); keys.add(new UserCacheKey(event.tenantId(), event.newEmail())); @@ -188,7 +190,7 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); @@ -646,6 +653,12 @@ public class UserServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findUserByIdAsync(tenantId, new UserId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return userDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 7e74c2a18b..db8cd875e3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.widget; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -47,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("WidgetTypeDaoService") @@ -54,8 +56,6 @@ import static org.thingsboard.server.dao.service.Validator.validateIds; public class WidgetTypeServiceImpl implements WidgetTypeService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; - public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias "; public static final String INCORRECT_WIDGETS_BUNDLE_ID = "Incorrect widgetsBundleId "; @Autowired @@ -281,6 +281,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { return Optional.ofNullable(findWidgetTypeById(tenantId, new WidgetTypeId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(widgetTypeDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; @@ -303,6 +309,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) { deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId())); } + }; private final PaginatedRemover bundleWidgetTypesRemover = new PaginatedRemover<>() { @@ -316,6 +323,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { protected void removeEntity(TenantId tenantId, WidgetTypeInfo widgetTypeInfo) { deleteWidgetType(tenantId, widgetTypeInfo.getId()); } + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index f057ce22ca..5b5930fb0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.widget; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -33,12 +34,10 @@ import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; -import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.resource.ImageService; -import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -48,13 +47,15 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; + @Service("WidgetsBundleDaoService") @Slf4j public class WidgetsBundleServiceImpl implements WidgetsBundleService { private static final int DEFAULT_WIDGETS_BUNDLE_LIMIT = 300; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; @Autowired private WidgetsBundleDao widgetsBundleDao; @@ -71,9 +72,6 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Autowired private ImageService imageService; - @Autowired - private ResourceService resourceService; - @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); @@ -92,9 +90,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { .entityId(result.getId()).created(widgetsBundle.getId() == null).build()); return result; } catch (Exception e) { - AbstractCachedEntityService.checkConstraintViolation(e, - "uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!"); - AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!"); + checkConstraintViolation(e, + "uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!", + "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!"); throw e; } } @@ -248,9 +246,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } if (widgetsBundleDescriptor.has("widgetTypeFqns")) { JsonNode widgetFqnsArrayJson = widgetsBundleDescriptor.get("widgetTypeFqns"); - widgetFqnsArrayJson.forEach(fqnJson -> { - widgetTypeFqns.add(fqnJson.asText()); - }); + widgetFqnsArrayJson.forEach(fqnJson -> widgetTypeFqns.add(fqnJson.asText())); } widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), widgetTypeFqns); }); @@ -274,23 +270,29 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(widgetsBundleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; } - private PaginatedRemover tenantWidgetsBundleRemover = - new PaginatedRemover() { + private final PaginatedRemover tenantWidgetsBundleRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, WidgetsBundle entity) { + deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId())); + } - @Override - protected void removeEntity(TenantId tenantId, WidgetsBundle entity) { - deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId())); - } - }; + }; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 6ccf2f6d95..4ccc5c9a2b 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -923,7 +923,7 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), - CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name) + CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); CREATE TABLE IF NOT EXISTS calculated_field_link ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 849528b091..1b8a08defc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -211,7 +211,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -319,7 +319,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() @@ -622,7 +622,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.MAJOR) .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm1 = result.getAlarm(); - alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -632,7 +632,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm2 = result.getAlarm(); alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis()); - alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -852,7 +852,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); @@ -980,7 +980,7 @@ public class AlarmServiceTest extends AbstractServiceTest { alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); Assert.assertEquals(0, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED)); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 4b830835fa..0e20f188b1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -249,7 +249,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> calculatedFieldService.save(calculatedField)) .isInstanceOf(DataValidationException.class) - .hasMessage("Calculated Field with such name is already in exists!"); + .hasMessage("Calculated field with such name and type already exists"); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java index 636b1a2ba5..d5c969b6f3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java @@ -19,7 +19,10 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; @@ -27,6 +30,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; @DaoSqlTest @@ -36,6 +40,8 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest { DeviceCredentialsService deviceCredentialsService; @Autowired DeviceService deviceService; + @MockitoBean + ApplicationEventPublisher eventPublisher; @Test public void testCreateDeviceCredentials() { @@ -185,5 +191,39 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest { Assert.assertEquals(deviceCredentials, foundDeviceCredentials); deviceService.deleteDevice(tenantId, savedDevice.getId()); } -} + @Test + public void testUpdateDeviceCredentialsWithSameValuesDoesNotPublishEvent() { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("My device"); + device.setType("default"); + Device savedDevice = deviceService.saveDevice(device); + + try { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); + Assert.assertNotNull(deviceCredentials); + + DeviceCredentials updatedCredentials = new DeviceCredentials(deviceCredentials.getId()); + updatedCredentials.setDeviceId(deviceCredentials.getDeviceId()); + updatedCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); + updatedCredentials.setCredentialsId(deviceCredentials.getCredentialsId()); + updatedCredentials.setCredentialsValue(deviceCredentials.getCredentialsValue()); + + Mockito.reset(eventPublisher); + + DeviceCredentials result = deviceCredentialsService.updateDeviceCredentials(tenantId, updatedCredentials); + + Assert.assertEquals(deviceCredentials.getCredentialsId(), result.getCredentialsId()); + Assert.assertEquals(deviceCredentials.getCredentialsType(), result.getCredentialsType()); + Assert.assertEquals(deviceCredentials.getCredentialsValue(), result.getCredentialsValue()); + Assert.assertEquals(deviceCredentials.getDeviceId(), result.getDeviceId()); + + Mockito.verify(eventPublisher, Mockito.never()).publishEvent(Mockito.any(ActionEntityEvent.class)); + + } finally { + deviceService.deleteDevice(tenantId, savedDevice.getId()); + } + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 39fb69ffce..70f2ff119d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -53,6 +53,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; public class OtaPackageServiceTest extends AbstractServiceTest { public static final String TITLE = "My firmware"; + public static final String TARGET_FW_VERSION = "fw.v.1.5.0-update"; private static final String FILE_NAME = "filename.txt"; private static final String VERSION = "v1.0"; private static final String CONTENT_TYPE = "text/plain"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index bb7bfad677..e3827d6a6e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; import java.util.Collections; @@ -52,6 +53,9 @@ public class RelationServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; + @Before public void before() { } @@ -628,48 +632,111 @@ public class RelationServiceTest extends AbstractServiceTest { } @Test - public void testFindByPathQuery() throws Exception { + public void testFindByPathQueryWithoutExceedingLimit() throws Exception { /* A └──[firstLevel, TO]→ B └──[secondLevel, TO]→ C - ├──[thirdLevel, FROM]→ D - ├──[thirdLevel, FROM]→ E - └──[thirdLevel, FROM]→ F + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N - 1}, where N is the limit */ - // rootEntity AssetId assetA = new AssetId(Uuids.timeBased()); - // firstLevelEntity AssetId assetB = new AssetId(Uuids.timeBased()); - // secondLevelEntity AssetId assetC = new AssetId(Uuids.timeBased()); - // thirdLevelEntities - AssetId assetD = new AssetId(Uuids.timeBased()); - AssetId assetE = new AssetId(Uuids.timeBased()); - AssetId assetF = new AssetId(Uuids.timeBased()); - EntityRelation firstLevelRelation = new EntityRelation(assetB, assetA, "firstLevel"); - EntityRelation secondLevelRelation = new EntityRelation(assetC, assetB, "secondLevel"); - EntityRelation thirdLevelRelation1 = new EntityRelation(assetC, assetD, "thirdLevel"); - EntityRelation thirdLevelRelation2 = new EntityRelation(assetC, assetE, "thirdLevel"); - EntityRelation thirdLevelRelation3 = new EntityRelation(assetC, assetF, "thirdLevel"); + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); - firstLevelRelation = saveRelation(firstLevelRelation); - secondLevelRelation = saveRelation(secondLevelRelation); - thirdLevelRelation1 = saveRelation(thirdLevelRelation1); - thirdLevelRelation2 = saveRelation(thirdLevelRelation2); - thirdLevelRelation3 = saveRelation(thirdLevelRelation3); + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); - List expectedRelations = List.of(thirdLevelRelation1, thirdLevelRelation2, thirdLevelRelation3); + int totalCreated = limit - 1; - EntityRelationPathQuery relationPathQuery = new EntityRelationPathQuery(assetA, List.of( + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") )); - List entityRelations = relationService.findByRelationPathQueryAsync(tenantId, relationPathQuery).get(); - assertThat(expectedRelations).containsExactlyInAnyOrderElementsOf(entityRelations); + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(totalCreated); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isEqualTo(allThirdLevelRelations); + } + + @Test + public void testFindByPathQueryWithExceedingLimit() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N + 20}, where N is the limit + */ + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); + + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); + + int totalCreated = limit + 20; + + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(limit); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isSubsetOf(allThirdLevelRelations); } @Test diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 0a15d6e8aa..7a4d3f1cfe 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -266,6 +266,33 @@ public class TestRestClient { .as(ArrayNode.class); } + + public ValidatableResponse deleteEntityAttributes(EntityId entityId, AttributeScope scope, String keys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityId", entityId.getId().toString()); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("scope", scope.name()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/{scope}") + .then() + .statusCode(HTTP_OK); + } + + public ValidatableResponse deleteEntityTimeseries(EntityId entityId, String keys, boolean deleteAllDataForKeys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("entityId", entityId.getId().toString()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .queryParam("deleteAllDataForKeys", Boolean.toString(deleteAllDataForKeys)) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete") + .then() + .statusCode(HTTP_OK); + } + public JsonNode getLatestTelemetry(EntityId entityId) { return given().spec(requestSpec) .get("/api/plugins/telemetry/" + entityId.getEntityType().name() + "/" + entityId.getId() + "/values/timeseries") @@ -378,6 +405,23 @@ public class TestRestClient { .as(EntityRelation.class); } + + public EntityRelation deleteEntityRelation(EntityId fromId, String relationType, EntityId toId) { + Map queryParams = new HashMap<>(); + queryParams.put("fromId", fromId.getId().toString()); + queryParams.put("fromType", fromId.getEntityType().name()); + queryParams.put("relationType", relationType); + queryParams.put("toId", toId.getId().toString()); + queryParams.put("toType", toId.getEntityType().name()); + return given().spec(requestSpec) + .queryParams(queryParams) + .delete("/api/v2/relation") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityRelation.class); + } + public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { return given().spec(requestSpec) .body(serverRpcPayload) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 5e8d367538..68c2c9462f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -23,6 +23,7 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.asset.Asset; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; @@ -419,6 +421,177 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.deleteCalculatedFieldIfExists(saved.getId()); } + @Test + public void testPropagationCalculatedField_withExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenA"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device With Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 1", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":12.5}")); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("t", arg)); + + cfg.setExpression("{\"testResult\": t * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNotNull().hasSize(1); + Map m1 = intKv(attrs1); + assertThat(m1).containsEntry("testResult", 25); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 25); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNullOrEmpty(); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 50); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenB"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device Without Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 3", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 4", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts))); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temperatureComputed", arg)); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNotNull(); + assertThat(temperature1.get("temperatureComputed")).isNotNull(); + assertThat(temperature1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature1.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityTimeseries(asset1.getId(), "temperatureComputed", true); + + // Update telemetry on device + long newTs = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs))); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNullOrEmpty(); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asInt()).isEqualTo(25); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + } + private CalculatedField createSimpleCalculatedField() { return createSimpleCalculatedField(device.getId()); } @@ -514,4 +687,12 @@ public class CalculatedFieldTest extends AbstractContainerTest { return m; } + private static Map intKv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asInt()); + } + return m; + } + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index edcbffcfbc..0a1b80f3d4 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -63,6 +63,8 @@ public interface RuleEngineAlarmService { AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index 678f3a015d..27cf108d1f 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -15,9 +15,6 @@ */ package org.thingsboard.rule.engine.api; -/** - * Created by ashvayka on 02.04.18. - */ public interface RuleEngineTelemetryService { void saveTimeseries(TimeseriesSaveRequest request); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java index 6f5898fe64..d25846c984 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -16,19 +16,27 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @Data @AllArgsConstructor +@NoArgsConstructor +@Builder public class TbAlarmResult { + boolean isCreated; boolean isUpdated; boolean isSeverityUpdated; boolean isCleared; Alarm alarm; + Long conditionRepeats; + Long conditionDuration; + public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { this.isCreated = isCreated; this.isUpdated = isUpdated; @@ -38,11 +46,13 @@ public class TbAlarmResult { public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { boolean isSeverityChanged = result.isSeverityChanged(); - return new TbAlarmResult( - result.isCreated(), - result.isModified() && !isSeverityChanged, - isSeverityChanged, - result.isCleared(), - result.getAlarm()); + return TbAlarmResult.builder() + .isCreated(result.isCreated()) + .isUpdated(result.isModified() && !isSeverityChanged) + .isSeverityUpdated(isSeverityChanged) + .isCleared(result.isCleared()) + .alarm(result.getAlarm()) + .build(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index d1016ee4fe..04ec95e60f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -41,6 +41,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CREATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_SEVERITY_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; @@ -78,7 +81,7 @@ public abstract class AbstractTbMsgPushNode extends Tb protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) { DataToFetch dataToFetch = config.getDataToFetch(); switch (dataToFetch) { - case ATTRIBUTES: - processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); - break; - case LATEST_TELEMETRY: - processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); - break; - case FIELDS: - processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true); - break; + case ATTRIBUTES -> processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); + case LATEST_TELEMETRY -> processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); + case FIELDS -> processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java index 189ec47ef1..a11e3333ff 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java @@ -17,8 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.Futures; +import com.google.common.base.Function; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -54,12 +53,12 @@ public abstract class TbAbstractNodeWithFetchTo AsyncFunction checkIfEntityIsPresentOrThrow(String message) { + protected Function checkIfEntityIsPresentOrThrow(String message) { return id -> { if (id == null || id.isNullUid()) { - return Futures.immediateFailedFuture(new NoSuchElementException(message)); + throw new NoSuchElementException(message); } - return Futures.immediateFuture(id); + return id; }; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index d779bacb77..02a1330a53 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -16,19 +16,22 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; +import java.util.NoSuchElementException; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + @RuleNode( type = ComponentType.ENRICHMENT, name = "customer attributes", @@ -44,8 +47,6 @@ import org.thingsboard.server.common.data.util.TbPair; ) public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode { - private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s"; - @Override protected TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class); @@ -56,10 +57,19 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode findEntityAsync(TbContext ctx, EntityId originator) { - return Futures.transformAsync(EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator), - checkIfEntityIsPresentOrThrow(String.format(CUSTOMER_NOT_FOUND_MESSAGE, originator.getId(), originator.getEntityType().getNormalName())), - ctx.getDbCallbackExecutor() - ); + if (originator.getEntityType() == EntityType.CUSTOMER) { + return immediateFuture((CustomerId) originator); + } + return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), originator) + .transform(customerIdOpt -> { + if (customerIdOpt.isEmpty()) { + throw new NoSuchElementException("Originator not found"); + } + if (customerIdOpt.get().isNullUid()) { + throw new IllegalStateException("Originator is not assigned to any customer"); + } + return customerIdOpt.get(); + }, ctx.getDbCallbackExecutor()); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index cf370114c3..9dbdba8ebe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -55,7 +55,7 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg) { - return Futures.transformAsync( + return Futures.transform( EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery()), checkIfEntityIsPresentOrThrow(RELATED_DEVICE_NOT_FOUND_MESSAGE), ctx.getDbCallbackExecutor()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index dffcaf35ba..fcd11a08f1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -58,7 +58,7 @@ public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode findEntityAsync(TbContext ctx, EntityId originator) { var relatedAttrConfig = (TbGetRelatedDataNodeConfiguration) config; - return Futures.transformAsync( + return Futures.transform( EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, relatedAttrConfig.getRelationsQuery()), checkIfEntityIsPresentOrThrow(RELATED_ENTITY_NOT_FOUND_MESSAGE), ctx.getDbCallbackExecutor()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java index 6062b9ea9c..68e3c7e2e7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.profile; +@Deprecated public enum AlarmEvalResult { FALSE, NOT_YET_TRUE, TRUE; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index a2a714a6df..1707f64bc7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -115,16 +115,12 @@ class AlarmRuleState { public AlarmEvalResult eval(DataSnapshot data) { boolean active = isActive(data, data.getTs()); - switch (spec.getType()) { - case SIMPLE: - return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; - case DURATION: - return evalDuration(data, active); - case REPEATING: - return evalRepeating(data, active); - default: - return AlarmEvalResult.FALSE; - } + return switch (spec.getType()) { + case SIMPLE -> (active && eval(alarmRule.getCondition(), data)) ? + AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + case DURATION -> evalDuration(data, active); + case REPEATING -> evalRepeating(data, active); + }; } private boolean isActive(DataSnapshot data, long eventTs) { @@ -600,4 +596,5 @@ class AlarmRuleState { return null; } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index c6cc39916e..219918c485 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -48,6 +48,7 @@ import java.util.function.BiFunction; @Data @Slf4j +@Deprecated class AlarmState { public static final String ERROR_MSG = "Failed to process alarm rule for Device [%s]: %s"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java index 33a6fe1631..b9ca93377f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +@Deprecated class DataSnapshot { private volatile boolean ready; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 4bd81050db..f78cd7c088 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -71,6 +71,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; @Slf4j +@Deprecated class DeviceState { private final boolean persistState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java index 3c2884d616..f6cdc9a781 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.profile; +@Deprecated public interface DynamicPredicateValueCtx { EntityKeyValue getTenantValue(String key); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java index 18fa81f63a..8962dff463 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; @Slf4j +@Deprecated public class DynamicPredicateValueCtxImpl implements DynamicPredicateValueCtx { private final TenantId tenantId; private CustomerId customerId; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java index 7561c51386..9742ffa234 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java @@ -20,6 +20,7 @@ import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; @EqualsAndHashCode +@Deprecated class EntityKeyValue { @Getter diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java index fffc66d02a..982a7f3b36 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java @@ -45,6 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +@Deprecated class ProfileState { private DeviceProfile deviceProfile; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java index d41a42efa3..08af665038 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import java.util.Set; +@Deprecated class SnapshotUpdate { @Getter diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index 2bb172bd5b..c40ac70618 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -51,17 +51,18 @@ import java.util.concurrent.TimeUnit; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "device profile", + name = "device profile (deprecated)", customRelations = true, relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, version = 1, configClazz = TbDeviceProfileNodeConfiguration.class, - nodeDescription = "Process device messages based on device profile settings", + nodeDescription = "Process device messages based on device profile settings (deprecated)", nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " + - "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", + "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", configDirective = "tbActionNodeDeviceProfileConfig", docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/" ) +@Deprecated public class TbDeviceProfileNode implements TbNode { private TbDeviceProfileNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java index a3180893d1..0605eed516 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class TbDeviceProfileNodeConfiguration implements NodeConfiguration { private boolean persistAlarmRulesState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java index 30aa4c443b..035d564f65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor +@Deprecated public class PersistedAlarmRuleState { private long lastEventTs; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java index dba8ba17a8..16d11485df 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; import java.util.Map; @Data +@Deprecated public class PersistedAlarmState { private Map createRuleStates; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java index 46f8a3b2ca..d4307e3955 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java @@ -20,6 +20,7 @@ import lombok.Data; import java.util.Map; @Data +@Deprecated public class PersistedDeviceState { Map alarmStates; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index c819b98466..9d91802f8b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -40,13 +40,13 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE; @RuleNode( type = ComponentType.ACTION, - name = "calculated fields", + name = "calculated fields and alarm rules", configClazz = EmptyNodeConfiguration.class, - nodeDescription = "Pushes incoming messages to calculated fields service", - nodeDetails = "Node enables the processing of calculated fields without persisting incoming messages to the database. " + - "By default, the processing of calculated fields is triggered by the save attributes and save time series nodes. " + + nodeDescription = "Pushes incoming messages to calculated fields and alarm rules services", + nodeDetails = "Node enables the processing of calculated fields and alarm rules without persisting incoming messages to the database. " + + "By default, the processing of calculated fields and alarm rules is triggered by the save attributes and save time series nodes. " + "This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " + - "fields independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", + "fields or alarm rules independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", configDirective = "tbNodeEmptyConfig", icon = "published_with_changes", docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/" diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index e3ab60a6e0..a97cc22d5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesAlarmOriginatorIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesByNameAndTypeLoader; -import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -36,6 +35,7 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; import java.util.NoSuchElementException; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; @@ -74,16 +74,20 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode getNewOriginator(TbContext ctx, TbMsg msg) { switch (config.getOriginatorSource()) { case CUSTOMER: - return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, msg.getOriginator()); + if (msg.getOriginator().getEntityType() == EntityType.CUSTOMER) { + return immediateFuture(msg.getOriginator()); + } + return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), msg.getOriginator()) + .transform(customerIdOpt -> customerIdOpt.orElse(null), ctx.getDbCallbackExecutor()); case TENANT: - return Futures.immediateFuture(ctx.getTenantId()); + return immediateFuture(ctx.getTenantId()); case RELATED: return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, msg.getOriginator(), config.getRelationsQuery()); case ALARM_ORIGINATOR: @@ -93,7 +97,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode findEntityIdAsync(TbContext ctx, EntityId originator) { - switch (originator.getEntityType()) { - case CUSTOMER: - return Futures.immediateFuture((CustomerId) originator); - case USER: - return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator)); - case ASSET: - return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator)); - case DEVICE: - return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator))); - default: - return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType())); - } - } - - private static ListenableFuture toCustomerIdAsync(TbContext ctx, ListenableFuture future) { - return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor()); - } - -} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java index b3afec500c..f27ea73ce6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java @@ -28,7 +28,7 @@ public class TestDbCallbackExecutor implements ListeningExecutor { try { return Futures.immediateFuture(task.call()); } catch (Exception e) { - throw new RuntimeException(e); + return Futures.immediateFailedFuture(e); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index 3f2c58f191..942b1ab5ce 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -16,7 +16,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -53,19 +53,19 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.user.UserService; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -73,19 +73,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbGetCustomerAttributeNodeTest { - private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); - private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + private final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); + private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + @Mock private TbContext ctxMock; @Mock @@ -93,21 +94,24 @@ public class TbGetCustomerAttributeNodeTest { @Mock private TimeseriesService timeseriesServiceMock; @Mock - private UserService userServiceMock; - @Mock - private AssetService assetServiceMock; - @Mock - private DeviceService deviceServiceMock; + private EntityService entityServiceMock; + private TbGetCustomerAttributeNode node; private TbGetEntityDataNodeConfiguration config; private TbNodeConfiguration nodeConfiguration; private TbMsg msg; @BeforeEach - public void setUp() { + public void setup() { node = new TbGetCustomerAttributeNode(); config = new TbGetEntityDataNodeConfiguration().defaultConfiguration(); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + lenient().when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); + lenient().when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); + lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock); } @Test @@ -237,12 +241,9 @@ public class TbGetCustomerAttributeNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(null)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(userId)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, userId)).thenReturn( + FluentFuture.from(immediateFuture(Optional.empty())) + ); // WHEN node.onMsg(ctxMock, msg); @@ -252,18 +253,13 @@ public class TbGetCustomerAttributeNodeTest { var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock, never()).tellSuccess(any()); - verify(ctxMock, times(1)) - .tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); + verify(ctxMock).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); var actualMessage = actualMessageCaptor.getValue(); var actualException = actualExceptionCaptor.getValue(); - var expectedExceptionMessage = String.format( - "Failed to find customer for entity with id: %s and type: %s", - userId.getId(), userId.getEntityType().getNormalName()); - assertEquals(msg, actualMessage); - assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals("Originator not found", actualException.getMessage()); assertInstanceOf(NoSuchElementException.class, actualException); } @@ -282,16 +278,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); - doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId())); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(attributesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(attributesList)); // WHEN node.onMsg(ctxMock, msg); @@ -299,7 +291,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgData = "{\"temp\":42," + @@ -329,16 +321,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId())); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, user.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(attributesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(attributesList)); // WHEN node.onMsg(ctxMock, msg); @@ -346,7 +334,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgMetaData = new TbMsgMetaData(Map.of( @@ -375,21 +363,18 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(customer.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(timeseriesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(timeseriesList)); // WHEN node.onMsg(ctxMock, msg); // THEN + verifyNoInteractions(entityServiceMock); + var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgData = "{\"temp\":42," + @@ -408,7 +393,7 @@ public class TbGetCustomerAttributeNodeTest { public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() { // GIVEN var asset = new Asset(new AssetId(UUID.randomUUID())); - asset.setCustomerId(new CustomerId(UUID.randomUUID())); + asset.setCustomerId(CUSTOMER_ID); prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); @@ -419,16 +404,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getAssetService()).thenReturn(assetServiceMock); - doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId())); - - when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); - when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(asset.getCustomerId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(timeseriesList)); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, asset.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(CUSTOMER_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + .thenReturn(immediateFuture(timeseriesList)); // WHEN node.onMsg(ctxMock, msg); @@ -436,7 +417,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgMetaData = new TbMsgMetaData(Map.of( diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 90c35e59b1..c16d09969b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -59,7 +59,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; @@ -85,14 +84,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -246,7 +243,6 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { node.onMsg(ctx, msg2); verify(ctx).tellSuccess(msg2); verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated"); - } @Test diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index 4826cfe297..da8816e824 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -15,8 +15,10 @@ */ package org.thingsboard.rule.engine.transform; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -35,6 +37,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; @@ -54,21 +57,24 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; import static org.thingsboard.rule.engine.transform.OriginatorSource.ALARM_ORIGINATOR; import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER; import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; @@ -93,16 +99,23 @@ public class TbChangeOriginatorNodeTest { @Mock private AssetService assetServiceMock; @Mock - private DeviceService deviceServiceMock; - @Mock private RelationService relationServiceMock; @Mock private RuleEngineAlarmService alarmServiceMock; + @Mock + private EntityService entityServiceMock; @BeforeEach - public void before() throws TbNodeException { + public void setup() { node = new TbChangeOriginatorNode(); config = new TbChangeOriginatorNodeConfiguration().defaultConfiguration(); + + lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); + lenient().when(ctxMock.getAssetService()).thenReturn(assetServiceMock); + lenient().when(ctxMock.getRelationService()).thenReturn(relationServiceMock); + lenient().when(ctxMock.getAlarmService()).thenReturn(alarmServiceMock); + lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock); } @Test @@ -172,8 +185,13 @@ public class TbChangeOriginatorNodeTest { } @Test - public void givenOriginatorSourceIsCustomer_whenOnMsg_thenTellSuccess() throws TbNodeException { - Device device = new Device(DEVICE_ID); + @DisplayName(""" + Given a device assigned to a customer and node configured to change originator to customer, + when processing the message, + then should change message originator from device to customer""") + public void givenDeviceAssignedToCustomer_whenProcessingMessage_thenChangesOriginatorToCustomer() throws TbNodeException { + // GIVEN + var device = new Device(DEVICE_ID); device.setCustomerId(CUSTOMER_ID); TbMsg msg = TbMsg.newMsg() @@ -186,16 +204,52 @@ public class TbChangeOriginatorNodeTest { .originator(CUSTOMER_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getDeviceService()).willReturn(deviceServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(deviceServiceMock.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(device); + given(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).willReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); + + given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); + + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID); + ArgumentCaptor actualMsg = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellSuccess(actualMsg.capture()); + assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); + } + + @Test + @DisplayName(""" + Given a customer as message originator and node configured to change originator to customer, + when processing the message, + then should keep the customer as originator""") + public void givenCustomerAsOriginator_whenProcessingMessage_thenKeepsCustomerAsOriginator() throws TbNodeException { + // GIVEN + var customer = new Customer(CUSTOMER_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(customer.getId()) + .metaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + var expectedMsg = msg.transform() + .originator(CUSTOMER_ID) + .build(); + given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN node.onMsg(ctxMock, msg); - then(deviceServiceMock).should().findDeviceById(TENANT_ID, DEVICE_ID); + // THEN then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID); ArgumentCaptor actualMsg = ArgumentCaptor.forClass(TbMsg.class); then(ctxMock).should().tellSuccess(actualMsg.capture()); @@ -216,8 +270,6 @@ public class TbChangeOriginatorNodeTest { .originator(TENANT_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); @@ -240,9 +292,6 @@ public class TbChangeOriginatorNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getRelationService()).willReturn(relationServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(relationServiceMock.findByQuery(any(TenantId.class), any(EntityRelationsQuery.class))).willReturn(Futures.immediateFuture(Collections.emptyList())); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); @@ -282,9 +331,6 @@ public class TbChangeOriginatorNodeTest { .originator(DEVICE_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(alarmServiceMock.findAlarmByIdAsync(any(TenantId.class), any(AlarmId.class))).willReturn(Futures.immediateFuture(alarm)); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); @@ -315,9 +361,6 @@ public class TbChangeOriginatorNodeTest { .originator(ASSET_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAssetService()).willReturn(assetServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(new Asset(ASSET_ID)); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); @@ -355,9 +398,6 @@ public class TbChangeOriginatorNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAssetService()).willReturn(assetServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(null); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java deleted file mode 100644 index 0be4e64ca2..0000000000 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.rule.engine.util; - -import com.google.common.util.concurrent.Futures; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.common.util.ListeningExecutor; -import org.thingsboard.rule.engine.TestDbCallbackExecutor; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.user.UserService; - -import java.util.EnumSet; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class EntitiesCustomerIdAsyncLoaderTest { - - private static final EnumSet SUPPORTED_ENTITY_TYPES = EnumSet.of( - EntityType.CUSTOMER, - EntityType.USER, - EntityType.ASSET, - EntityType.DEVICE - ); - private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); - @Mock - private TbContext ctxMock; - @Mock - private UserService userServiceMock; - @Mock - private AssetService assetServiceMock; - @Mock - private DeviceService deviceServiceMock; - - @Test - public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var customer = new Customer(new CustomerId(UUID.randomUUID())); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get(); - - // THEN - assertEquals(customer.getId(), actualCustomerId); - } - - @Test - public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var user = new User(new UserId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - user.setCustomerId(expectedCustomerId); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var asset = new Asset(new AssetId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - asset.setCustomerId(expectedCustomerId); - - when(ctxMock.getAssetService()).thenReturn(assetServiceMock); - doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var device = new Device(new DeviceId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - device.setCustomerId(expectedCustomerId); - - when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); - doReturn(device).when(deviceServiceMock).findDeviceById(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() { - for (var entityType : EntityType.values()) { - if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) { - var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); - - var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType; - - var exception = assertThrows(ExecutionException.class, - () -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get()); - - assertInstanceOf(TbNodeException.class, exception.getCause()); - assertEquals(expectedExceptionMsg, exception.getMessage()); - } - } - } - -} diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch new file mode 100644 index 0000000000..c8090076cc --- /dev/null +++ b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch @@ -0,0 +1,97 @@ +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs +index bf5480b..02c8246 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs +@@ -29,10 +29,10 @@ export class HexComponent { + } + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{ + type: Component, +- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGV4LWlucHV0LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsdUJBQXVCLEVBQUUsZ0JBQWdCLEVBQWUsS0FBSyxFQUFFLEtBQUssRUFBZSxNQUFNLGVBQWUsQ0FBQztBQUM3SCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0saUNBQWlDLENBQUM7O0FBY3hELE1BQU0sT0FBTyxZQUFZO0lBWHpCO1FBYVcsVUFBSyxHQUF1QixLQUFLLENBQUMsUUFBUSxFQUFTLENBQUM7UUFFcEQsaUJBQVksR0FBeUIsS0FBSyxDQUFtQixLQUFLLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxnQkFBZ0IsRUFBRSxDQUFDLENBQUM7UUFFckgsZ0JBQVcsR0FBd0IsS0FBSyxDQUFTLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0tBMkJwRjtJQXpCRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxXQUFXLEVBQUUsR0FBRyxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ2xJLENBQUM7SUFFTSxhQUFhLENBQUMsS0FBb0IsRUFBRSxVQUFrQjtRQUN6RCxNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsV0FBVyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUV4RCxJQUNBLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxLQUFLLEVBQUUsSUFBSSxLQUFLLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxLQUFLLE9BQU8sQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUFDO2VBQ2xGLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDMUMsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNoQyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBRWhDOzs7OztlQUtHO1lBQ0gsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsR0FBRyxDQUFDLEtBQUssS0FBSyxJQUFJLElBQUksQ0FBQyxLQUFLLEtBQUssS0FBSyxFQUFFLENBQUM7Z0JBQ3ZFLE1BQU0sUUFBUSxHQUFHLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxFQUFFLENBQUMsQ0FBQztnQkFDeEMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDN0IsQ0FBQztRQUNMLENBQUM7SUFDTCxDQUFDOzhHQWhDUSxZQUFZO2tHQUFaLFlBQVksZ2dCQ2Z6Qiw0TUFLTTs7MkZEVU8sWUFBWTtrQkFYeEIsU0FBUzsrQkFDSSxxQkFBcUIsbUJBT2QsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ29tcG9uZW50LCBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSwgYm9vbGVhbkF0dHJpYnV0ZSwgSW5wdXRTaWduYWwsIGlucHV0LCBtb2RlbCwgTW9kZWxTaWduYWwgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcclxuaW1wb3J0IHsgQ29sb3IgfSBmcm9tICcuLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuXHJcblxyXG5AQ29tcG9uZW50KHtcclxuICAgIHNlbGVjdG9yOiBgaGV4LWlucHV0LWNvbXBvbmVudGAsXHJcbiAgICB0ZW1wbGF0ZVVybDogYC4vaGV4LWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hleC1pbnB1dC5jb21wb25lbnQuc2Nzc2BcclxuICAgIF0sXHJcbiAgICBjaGFuZ2VEZXRlY3Rpb246IENoYW5nZURldGVjdGlvblN0cmF0ZWd5Lk9uUHVzaCxcclxuICAgIHN0YW5kYWxvbmU6IHRydWVcclxufSlcclxuZXhwb3J0IGNsYXNzIEhleENvbXBvbmVudCB7XHJcblxyXG4gICAgcHVibGljIGNvbG9yOiBNb2RlbFNpZ25hbDxDb2xvcj4gPSBtb2RlbC5yZXF1aXJlZDxDb2xvcj4oKTtcclxuXHJcbiAgICBwdWJsaWMgbGFiZWxWaXNpYmxlOiBJbnB1dFNpZ25hbDxib29sZWFuPiA9IGlucHV0PGJvb2xlYW4sIGJvb2xlYW4+KGZhbHNlLCB7IGFsaWFzOiAnbGFiZWwnLCB0cmFuc2Zvcm06IGJvb2xlYW5BdHRyaWJ1dGUgfSk7XHJcblxyXG4gICAgcHVibGljIHByZWZpeFZhbHVlOiBJbnB1dFNpZ25hbDxzdHJpbmc+ID0gaW5wdXQ8c3RyaW5nPignJywgeyBhbGlhczogJ3ByZWZpeCcgfSk7XHJcblxyXG4gICAgcHVibGljIGdldCB2YWx1ZSgpIHtcclxuICAgICAgICByZXR1cm4gdGhpcy5wcmVmaXhWYWx1ZSgpICsgKHRoaXMuY29sb3IoKSA/IHRoaXMuY29sb3IoKS50b0hleFN0cmluZyh0aGlzLmNvbG9yKCkuZ2V0UmdiYSgpLmFscGhhIDwgMSkucmVwbGFjZSgnIycsICcnKSA6ICcnKTtcclxuICAgIH1cclxuXHJcbiAgICBwdWJsaWMgb25JbnB1dENoYW5nZShldmVudDogS2V5Ym9hcmRFdmVudCwgaW5wdXRWYWx1ZTogc3RyaW5nKTogdm9pZCB7XHJcbiAgICAgICAgY29uc3QgdmFsdWUgPSBpbnB1dFZhbHVlLnRvTG93ZXJDYXNlKCkucmVwbGFjZSgnIycsICcnKTtcclxuXHJcbiAgICAgICAgaWYgKFxyXG4gICAgICAgICgoZXZlbnQua2V5Q29kZSA9PT0gMTMgfHwgZXZlbnQua2V5LnRvTG93ZXJDYXNlKCkgPT09ICdlbnRlcicpICYmIHZhbHVlLmxlbmd0aCA9PT0gMylcclxuICAgICAgICB8fCB2YWx1ZS5sZW5ndGggPT09IDYgfHwgdmFsdWUubGVuZ3RoID09PSA4KSB7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleCA9IHBhcnNlSW50KHZhbHVlLCAxNik7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleFN0ciA9IGhleC50b1N0cmluZygxNik7XHJcblxyXG4gICAgICAgICAgICAvKipcclxuICAgICAgICAgICAgICogaWYgdmFsdWUgaXMgdmFsaWRcclxuICAgICAgICAgICAgICogY2hhbmdlIGNvbG9yIGVsc2UgZG8gbm90aGluZ1xyXG4gICAgICAgICAgICAgKiBhZnRlciBwYXJzaW5nIG51bWJlciBsZWFkaW5nIDAgaXMgcmVtb3ZlZCxcclxuICAgICAgICAgICAgICogY29tcGFyZSBsZW5ndGggYW5kIGFkZCBsZWFkaW5nIDAgYmVmb3JlIGNvbXBhcmluZyB0d28gdmFsdWVzXHJcbiAgICAgICAgICAgICAqL1xyXG4gICAgICAgICAgICBpZiAoaGV4U3RyLnBhZFN0YXJ0KHZhbHVlLmxlbmd0aCwgJzAnKSA9PT0gdmFsdWUgJiYgdGhpcy52YWx1ZSAhPT0gdmFsdWUpIHtcclxuICAgICAgICAgICAgICAgIGNvbnN0IG5ld0NvbG9yID0gbmV3IENvbG9yKGAjJHt2YWx1ZX1gKTtcclxuICAgICAgICAgICAgICAgIHRoaXMuY29sb3Iuc2V0KG5ld0NvbG9yKTtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgIH1cclxufVxyXG4iLCI8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgI2VsUmVmIHR5cGU9XCJ0ZXh0XCIgW3ZhbHVlXT1cInZhbHVlXCIgKGtleXVwKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCBlbFJlZi52YWx1ZSlcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkhFWDwvc3Bhbj5cclxuICAgIH1cclxuPC9kaXY+Il19 +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs +index 2454b27..dd007ce 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs +@@ -21,10 +21,10 @@ export class HslaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{ + type: Component, +- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHNsYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvaHNsYS1pbnB1dC9oc2xhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oc2xhLWlucHV0L2hzbGEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxVQUFVLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDO1FBQy9ELE1BQU0sU0FBUyxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDeEUsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYGhzbGEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9oc2xhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hzbGEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBIc2xhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0SHNsYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnSCcgfCAnUycgfCAnTCcgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgaHVlID0gY29sb3IgPT09ICdIJyA/IG5ld1ZhbHVlIDogdmFsdWUuaHVlO1xyXG4gICAgICAgIGNvbnN0IHNhdHVyYXRpb24gPSBjb2xvciA9PT0gJ1MnID8gbmV3VmFsdWUgOiB2YWx1ZS5zYXR1cmF0aW9uO1xyXG4gICAgICAgIGNvbnN0IGxpZ2h0bmVzcyA9IGNvbG9yID09PSAnTCcgPyBuZXdWYWx1ZSA6IHZhbHVlLmxpZ2h0bmVzcztcclxuICAgICAgICBjb25zdCBhbHBoYSA9IGNvbG9yID09PSAnQScgPyBuZXdWYWx1ZSA6IHZhbHVlLmFscGhhO1xyXG5cclxuICAgICAgICBjb25zdCBuZXdDb2xvciA9IG5ldyBDb2xvcigpLnNldEhzbGEoaHVlLCBzYXR1cmF0aW9uLCBsaWdodG5lc3MsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIzNjBcIiBbdmFsdWVdPVwidmFsdWU/LmdldEh1ZSgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdIJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkg8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjEwMFwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0U2F0dXJhdGlvbigpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnUycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5TPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIxMDBcIiBbdmFsdWVdPVwidmFsdWU/LmdldExpZ2h0bmVzcygpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnTCcpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5MPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0= +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs +index 6d976e8..8989aad 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs +@@ -21,10 +21,10 @@ export class RgbaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{ + type: Component, +- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmdiYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvcmdiYS1pbnB1dC9yZ2JhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9yZ2JhLWlucHV0L3JnYmEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxLQUFLLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDO1FBQ3JELE1BQU0sSUFBSSxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQztRQUNuRCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDOUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYHJnYmEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9yZ2JhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL3JnYmEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBSZ2JhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0UmdiYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnUicgfCAnRycgfCAnQicgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgcmVkID0gY29sb3IgPT09ICdSJyA/IG5ld1ZhbHVlIDogdmFsdWUucmVkO1xyXG4gICAgICAgIGNvbnN0IGdyZWVuID0gY29sb3IgPT09ICdHJyA/IG5ld1ZhbHVlIDogdmFsdWUuZ3JlZW47XHJcbiAgICAgICAgY29uc3QgYmx1ZSA9IGNvbG9yID09PSAnQicgPyBuZXdWYWx1ZSA6IHZhbHVlLmJsdWU7XHJcbiAgICAgICAgY29uc3QgYWxwaGEgPSBjb2xvciA9PT0gJ0EnID8gbmV3VmFsdWUgOiB2YWx1ZS5hbHBoYTtcclxuXHJcbiAgICAgICAgY29uc3QgbmV3Q29sb3IgPSBuZXcgQ29sb3IoKS5zZXRSZ2JhKHJlZCwgZ3JlZW4sIGJsdWUsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldFJlZCgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdSJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPlI8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjI1NVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0R3JlZW4oKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnRycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5HPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldEJsdWUoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQicpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5CPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0= +diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +index a3b270c..40bea7f 100644 +--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs ++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +@@ -1123,11 +1123,11 @@ class RgbaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{ + type: Component, +- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + class HslaComponent { +@@ -1149,11 +1149,11 @@ class HslaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{ + type: Component, +- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + class HexComponent { +@@ -1184,11 +1184,11 @@ class HexComponent { + } + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{ + type: Component, +- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + const OpacityAnimation = trigger('opacityAnimation', [ diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index d6612f2427..6e4d324b5b 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,6 +31,7 @@ export interface SysParamsState { maxDebugModeDurationMinutes: number; maxDataPointsPerRollingArg: number; maxArgumentsPerCF: number; + minAllowedDeduplicationIntervalInSecForCF: number; minAllowedScheduledUpdateIntervalInSecForCF: number; maxRelationLevelPerCfArgument: number; ruleChainDebugPerTenantLimitsConfiguration?: string; diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 8cfbc04197..777cf5308e 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -33,6 +33,7 @@ const emptyUserAuthState: AuthPayload = { mobileQrEnabled: false, maxResourceSize: 0, maxArgumentsPerCF: 0, + minAllowedDeduplicationIntervalInSecForCF: 0, minAllowedScheduledUpdateIntervalInSecForCF: 0, maxRelationLevelPerCfArgument: 0, maxDataPointsPerRollingArg: 0, diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts index f8bbcc3241..5090af97ec 100644 --- a/ui-ngx/src/app/core/guards/auth.guard.ts +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -131,7 +131,7 @@ export class AuthGuard { } } if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) { - this.mobileService.handleMobileNavigation(path, params); + this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams); return of(false); } if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index 55eaa16b90..36bb931f93 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -21,9 +21,9 @@ import { HttpClient } from '@angular/common/http'; import { AdminSettings, AutoCommitSettings, - MailConfigTemplate, FeaturesInfo, JwtSettings, + MailConfigTemplate, MailServerSettings, RepositorySettings, RepositorySettingsInfo, diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 66c0cb609e..631550ae9e 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -15,11 +15,15 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; -import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models'; +import { + CalculatedField, + CalculatedFieldTestScriptInputParams, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; import { PageLink } from '@shared/models/page/page-link'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityTestScriptResult } from '@shared/models/entity.models'; @@ -46,9 +50,8 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/core/http/git-hub.service.ts b/ui-ngx/src/app/core/http/git-hub.service.ts new file mode 100644 index 0000000000..b6a927dab3 --- /dev/null +++ b/ui-ngx/src/app/core/http/git-hub.service.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class GitHubService { + + constructor( + private http: HttpClient + ) { } + + public getGitHubStar(config?: RequestConfig): Observable { + return this.http.get('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe( + catchError(() => of({})), + map((res: any) => res?.stargazers_count ?? 0) + ) + } +} diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 6fcb7c3602..aceb564ec2 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -106,9 +106,9 @@ export class MobileService { } } - public handleMobileNavigation(path?: string, params?: Params) { + public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) { if (this.mobileApp) { - this.mobileChannel.callHandler(navigationHandler, path, params); + this.mobileChannel.callHandler(navigationHandler, path, params, queryParams); } } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 82382a08eb..c68f26d56f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -34,8 +34,10 @@ import { } from '@shared/models/js-function.models'; import { DomSanitizer } from '@angular/platform-browser'; import { SecurityContext } from '@angular/core'; +import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'; const varsRegex = /\${([^}]*)}/g; +const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; export function onParentScrollOrWindowResize(el: Node): Observable { const scrollSubject = new Subject(); @@ -1033,3 +1035,11 @@ export const trimDefaultValues = (input: Record, defaults: Record { + if (isUndefinedOrNull(control.value) || (typeof control.value === 'string' && control.value.length === 0)) { + return null; + } + return emailRegex.test(control.value) ? null : {email: true}; +}; + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html new file mode 100644 index 0000000000..9e5036f6ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html @@ -0,0 +1,49 @@ + +
+ +

{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}

+ +
+
+
+ + + + +
+
+
+ + @if (!data.readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts new file mode 100644 index 0000000000..8a55f52678 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts @@ -0,0 +1,60 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; + +export interface AlarmRuleDetailsDialogData { + alarmDetails: string; + readonly: boolean; +} + +@Component({ + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './alarm-rule-details-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class AlarmRuleDetailsDialogComponent extends DialogComponent { + + alarmDetailsControl: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.alarmDetailsControl = this.fb.control(this.data.alarmDetails); + if (this.data.readonly) { + this.alarmDetailsControl.disable(); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmDetailsControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html new file mode 100644 index 0000000000..04423edea6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -0,0 +1,164 @@ + +
+ +

{{ 'alarm-rule.alarm-rule' | translate}}

+ +
+ +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'alarm-rule.alarm-type' | translate }} + + @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { + + @if (fieldFormGroup.get('name').hasError('required')) { + {{ 'alarm-rule.alarm-type-required' | translate }} + } @else if (fieldFormGroup.get('name').hasError('pattern')) { + {{ 'alarm-rule.alarm-type-pattern' | translate }} + } @else if (fieldFormGroup.get('name').hasError('maxlength')) { + {{ 'alarm-rule.alarm-type-max-length' | translate }} + } + + } + + +
+
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-alarm-rules' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-alarm-rule' | translate }}
+
+
+ + +
+ +
+
+ alarm-rule.no-clear-alarm-rule +
+
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + alarm-rule.alarm-rule-relation-types-list + + + {{key}} + close + + + + + + } +
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss new file mode 100644 index 0000000000..04e46fd3d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.calculated-field-dialog-container { + width: 869px; + max-width: 100%; +} + +.clear-alarm-rule { + border: 1px solid rgba(0, 0, 0, 0.12); + border-left-width: 4px; + border-left-color: green; + border-radius: 4px; + padding: 8px; + min-width: 0; +} + +.button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts new file mode 100644 index 0000000000..0ea3bd036e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -0,0 +1,179 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; +import { MatChipInputEvent } from "@angular/material/chips"; +import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; +import { deepTrim } from "@core/utils"; + +export interface AlarmRuleDialogData { + value?: CalculatedField; + buttonTitle: string; + entityId: EntityId; + tenantId: string; + entityName?: string; + ownerId: EntityId; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + isDirty?: boolean; +} + +@Component({ + selector: 'tb-alarm-rule-dialog', + templateUrl: './alarm-rule-dialog.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AlarmRuleDialogComponent extends DialogComponent { + + fieldFormGroup = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + type: [CalculatedFieldType.ALARM], + debugSettings: [], + configuration: this.fb.group({ + arguments: this.fb.control({}), + propagate: [false], + propagateToOwner: [false], + propagateToTenant: [false], + propagateRelationTypes: [null], + createRules: [null], + clearRule: [null], + }), + }); + + additionalDebugActionConfig = this.data.value?.id ? { + ...this.data.additionalDebugActionConfig, + action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), + } : null; + + readonly EntityType = EntityType; + readonly CalculatedFieldType = CalculatedFieldType; + readonly ScriptLanguage = ScriptLanguage; + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, + protected dialogRef: MatDialogRef, + private calculatedFieldsService: CalculatedFieldsService, + private destroyRef: DestroyRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.observeIsLoading(); + this.applyDialogData(); + } + + get configFormGroup(): FormGroup { + return this.fieldFormGroup.get('configuration') as FormGroup; + } + + get arguments(): Record { + return this.fieldFormGroup.get('configuration.arguments').value; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.fieldFormGroup.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + } + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + removeRelationType(key: string): void { + const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.configFormGroup.get('propagateRelationTypes').setValue(keys); + } + } + + addRelationType(event: MatChipInputEvent): void { + const input = event.chipInput.inputElement; + let value = (event.value ?? '').trim(); + if (value) { + let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? []; + if (keys.indexOf(value) === -1) { + keys.push(value); + this.configFormGroup.get('propagateRelationTypes').setValue(keys); + } + } + if (input) { + input.value = ''; + } + } + + get fromGroupValue(): CalculatedField { + return deepTrim(this.fieldFormGroup.value as CalculatedField); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.fieldFormGroup.valid) { + const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + + this.calculatedFieldsService.saveCalculatedField(alarmRule) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(calculatedField => this.dialogRef.close(calculatedField)); + } + } + + private applyDialogData(): void { + const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); + } + + private observeIsLoading(): void { + this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { + if (loading) { + this.fieldFormGroup.disable({emitEvent: false}); + } else { + this.fieldFormGroup.enable({emitEvent: false}); + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts new file mode 100644 index 0000000000..7acfca7b0d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component"; +import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component"; +import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component"; +import { + CfAlarmRuleConditionDialogComponent +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component"; +import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { + EntityDebugSettingsButtonComponent +} from "@home/components/entity/debug/entity-debug-settings-button.component"; +import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component"; +import { + CalculatedFieldArgumentsTableModule +} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module"; +import { + AlarmRuleFilterPredicateListComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component"; +import { + AlarmRuleFilterPredicateComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component"; +import { + AlarmRuleFilterPredicateValueComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component"; +import { + AlarmRuleComplexFilterPredicateDialogComponent +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component"; +import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; + +@NgModule({ + declarations: [ + AlarmRuleDialogComponent, + CreateCfAlarmRulesComponent, + CfAlarmRuleComponent, + CfAlarmRuleConditionComponent, + CfAlarmRuleConditionDialogComponent, + CfAlarmScheduleComponent, + CfAlarmScheduleDialogComponent, + AlarmRuleFilterTextComponent, + AlarmRuleFilterListComponent, + AlarmRuleFilterDialogComponent, + AlarmRuleFilterPredicateListComponent, + AlarmRuleFilterPredicateComponent, + AlarmRuleFilterPredicateValueComponent, + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleDetailsDialogComponent, + ], + imports: [ + CommonModule, + SharedModule, + EntityDebugSettingsButtonComponent, + CalculatedFieldArgumentsTableModule + ], + exports: [ + AlarmRuleDialogComponent, + ] +}) +export class AlarmRuleModule { } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts new file mode 100644 index 0000000000..5167e32853 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { PageLink } from '@shared/models/page/page-link'; +import { Observable, of } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { DestroyRef, Renderer2 } from '@angular/core'; +import { EntityDebugSettings } from '@shared/models/entity.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { catchError, filter, switchMap } from 'rxjs/operators'; +import { + ArgumentEntityType, + CalculatedField, + CalculatedFieldAlarmRule, + CalculatedFieldType, +} from '@shared/models/calculated-field.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { + AlarmRuleDialogComponent, + AlarmRuleDialogData +} from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { + CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogData +} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; +import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; +import { UtilsService } from "@core/services/utils.service"; + +export class AlarmRulesTableConfig extends EntityTableConfig { + + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + additionalDebugActionConfig = { + title: this.translate.instant('calculated-fields.see-debug-events'), + action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + }; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe, + public entityId: EntityId = null, + private store: Store, + private destroyRef: DestroyRef, + private renderer: Renderer2, + public entityName: string, + private ownerId: EntityId = null, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + ) { + super(); + this.tableTitle = this.translate.instant('alarm-rule.alarm-rules'); + this.detailsPanelEnabled = false; + this.pageMode = false; + this.entityType = EntityType.CALCULATED_FIELD; + this.entityTranslations = { + type: 'alarm-rule.alarm-rule', + typePlural: 'alarm-rule.alarm-rules', + list: 'alarm-rule.list', + add: 'action.add', + noEntities: 'alarm-rule.no-found', + search: 'action.search', + selectedEntities: 'alarm-rule.selected-fields' + }; + + this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); + this.addEntity = this.getCalculatedAlarmDialog.bind(this); + this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); + this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); + this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); + this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); + this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + this.addActionDescriptors = [ + { + name: this.translate.instant('alarm-rule.create'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.getTable().addEntity($event) + }, + { + name: this.translate.instant('alarm-rule.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: () => this.importCalculatedField() + } + ]; + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%', + entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), + () => ({}), false)); + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '70px', + entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); + + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: (event$, entity) => this.exportAlarmRule(event$, entity), + }, + { + name: this.translate.instant('entity-view.events'), + icon: 'mdi:clipboard-text-clock', + isEnabled: () => true, + onAction: (_, entity) => this.openDebugEventsDialog(entity), + }, + { + name: '', + nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), + icon: 'mdi:bug', + isEnabled: () => true, + iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), + }, + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: (_, entity) => this.editCalculatedField(entity), + } + ); + } + + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); + } + + onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + const { debugSettings = {}, id } = calculatedField; + const additionalActionConfig = { + ...this.additionalDebugActionConfig, + action: () => this.openDebugEventsDialog(calculatedField) + }; + if ($event) { + $event.stopPropagation(); + } + + const { viewContainerRef, renderer } = this.entityDebugSettingsService; + if (!viewContainerRef || !renderer) { + this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; + } + + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings, + debugConfig: { + entityType: EntityType.CALCULATED_FIELD, + entityLabel: 'alarm-rule.alarm-rule', + additionalActionConfig, + }, + onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) + }, $event.target as Element); + } + + private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { + return this.dialog.open(AlarmRuleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + value, + buttonTitle, + entityId: this.entityId, + tenantId: this.tenantId, + entityName: this.entityName, + ownerId: this.ownerId, + additionalDebugActionConfig: this.additionalDebugActionConfig, + isDirty, + }, + enterAnimationDuration: isDirty ? 0 : null, + }) + .afterClosed() + .pipe(filter(Boolean)); + } + + private openDebugEventsDialog(calculatedField: CalculatedField): void { + this.dialog.open(CalculatedFieldDebugDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + tenantId: this.tenantId, + value: calculatedField, + getTestScriptDialogFn: null, + } + }) + .afterClosed() + .subscribe(); + } + + private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { + if ($event) { + $event.stopPropagation(); + } + this.importExportService.exportCalculatedField(calculatedField.id.id); + } + + private importCalculatedField(): void { + this.importExportService.openCalculatedFieldImportDialog() + .pipe( + filter(Boolean), + switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + filter(Boolean), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => this.updateData()); + } + + private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { + if (calculatedField.type === CalculatedFieldType.GEOFENCING) { + calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => { + const arg = calculatedField.configuration.zoneGroups[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } else { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } + + return calculatedField; + } + + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { + this.calculatedFieldsService.getCalculatedFieldById(id).pipe( + switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), + catchError(() => of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => this.updateData()); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html new file mode 100644 index 0000000000..e9eebb226c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html @@ -0,0 +1,20 @@ + +@if (alarmRulesTableConfig) { + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss new file mode 100644 index 0000000000..0e0dacc392 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table { + --mat-sidenav-content-background-color: white; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts new file mode 100644 index 0000000000..4ade1c5461 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + effect, + input, + Renderer2, + ViewChild, +} from '@angular/core'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; +import { UtilsService } from "@core/services/utils.service"; + +@Component({ + selector: 'tb-alarm-rules-table', + templateUrl: './alarm-rules-table.component.html', + styleUrls: ['./alarm-rules-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService] +}) +export class AlarmRulesTableComponent { + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + active = input(); + entityId = input(); + entityName = input(); + ownerId = input(); + + alarmRulesTableConfig: AlarmRulesTableConfig; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private translate: TranslateService, + private dialog: MatDialog, + private store: Store, + private datePipe: DatePipe, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + private destroyRef: DestroyRef) { + + effect(() => { + if (this.active()) { + this.alarmRulesTableConfig = new AlarmRulesTableConfig( + this.calculatedFieldsService, + this.translate, + this.dialog, + this.datePipe, + this.entityId(), + this.store, + this.destroyRef, + this.renderer, + this.entityName(), + this.ownerId(), + this.importExportService, + this.entityDebugSettingsService, + this.utilsService, + ); + this.cd.markForCheck(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..d2f98ecd85 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -0,0 +1,188 @@ + +
+ +

{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}

+ +
+ + {{ 'alarm-rule.expression-type.simple' | translate }} + {{ 'alarm-rule.expression-type.tbel' | translate }} + +
+ +
+
+
+
+ @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+
+
{{ 'alarm-rule.argument-filters' | translate }}
+ + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
+ + +
+ } @else { +
+
+ {{ 'alarm-rule.script' | translate }} +
+ +
{{ 'alarm-rule.expression-type.tbel' | translate }} +
+
+
+ } +
+
+
{{ 'alarm-rule.condition-settings' | translate }}
+ + alarm-rule.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) { +
+
+
{{ 'alarm-rule.value' | translate }}
+ + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
+
+
+ +
+
+ +
+
+ + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'alarm-rule.condition-duration-time-unit-required' | translate }} + + +
+
+
+ } @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) { +
+
+
{{ 'alarm-rule.value' | translate }}
+ + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
+
+
+ +
+
+ +
+
+
+ } +
+
+
+
+ + @if (!readonly) { + + } +
+ + +
+ + + {{ defaultValuePlaceholder | translate }} + @if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) { + {{ defaultValueRequiredError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { + {{ defaultValuePatternError | translate }} + } + +
+
+ + + + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..48b290b32c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -0,0 +1,226 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from "@shared/models/rule-node.models"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleConditionTypeTranslationMap, + AlarmRuleExpressionType +} from "@shared/models/alarm-rule.models"; +import { + CalculatedFieldArgument, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights +} from "@shared/models/calculated-field.models"; +import { TbEditorCompleter } from "@shared/models/ace/completion.models"; +import { AceHighlightRules } from "@shared/models/ace/ace.models"; +import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; + +export interface CfAlarmRuleConditionDialogData { + readonly: boolean; + condition: AlarmRuleCondition; + arguments?: Record; +} + +@Component({ + selector: 'tb-cf-alarm-rule-condition-dialog', + templateUrl: './cf-alarm-rule-condition-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class CfAlarmRuleConditionDialogComponent extends DialogComponent { + + AlarmRuleExpressionType = AlarmRuleExpressionType; + + timeUnits = Object.values(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.values(AlarmRuleConditionType); + AlarmConditionType = AlarmRuleConditionType; + alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap; + readonly = this.data.readonly; + condition = this.data.condition; + + conditionFormGroup = this.fb.group({ + expression: this.fb.group({ + type: [AlarmRuleExpressionType.SIMPLE], + expression: ['', [Validators.required]], + operation: [ComplexOperation.AND], + filters: [null], + }), + type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required), + unit: this.fb.control(TimeUnit.SECONDS, Validators.required), + value: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + count: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + }); + + readonly scriptLanguage = ScriptLanguage; + + defaultValuePlaceholder = ''; + defaultValueRequiredError = ''; + defaultValueRangeError = ''; + defaultValuePatternError = ''; + + durationDynamicModeControl = this.fb.control(false); + repeatingDynamicModeControl = this.fb.control(false); + + ComplexOperation = ComplexOperation; + complexOperationTranslationMap = complexOperationTranslationMap; + + functionArgs: Array; + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; + + arguments = this.data.arguments; + argumentsList: Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; + this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments); + this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments); + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + + this.conditionFormGroup.patchValue({ + expression: { + type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE, + expression: this.condition?.expression?.expression ?? null, + filters: this.condition?.expression?.filters ?? [], + operation: this.condition?.expression?.operation ?? ComplexOperation.AND + }, + type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE, + unit: this.condition?.unit ?? TimeUnit.SECONDS, + value: { + staticValue: this.condition?.value?.staticValue, + dynamicValueArgument: this.condition?.value?.dynamicValueArgument, + }, + count: { + staticValue: this.condition?.count?.staticValue ?? null, + dynamicValueArgument: this.condition?.count?.dynamicValueArgument + } + }, {emitEvent: false}); + + this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); + this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false}); + + this.conditionFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateValidators(type, true); + }); + + this.conditionFormGroup.get('expression.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateExpressionTypeValidator(type); + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + }); + + this.durationDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode); + }); + this.repeatingDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode); + }); + + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); + } + + updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { + const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count'); + if (dynamicValue) { + control.get('staticValue').disable({emitEvent: false}); + control.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + control.get('staticValue').enable({emitEvent: false}); + control.get('dynamicValueArgument').disable({emitEvent: false}); + } + } + + updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { + if (type === 'SIMPLE') { + this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false}); + } else { + this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false}); + } + } + + private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { + switch (type) { + case AlarmRuleConditionType.DURATION: + this.conditionFormGroup.get('unit').enable({emitEvent: false}); + this.conditionFormGroup.get('value').enable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.durationDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern'; + break; + case AlarmRuleConditionType.REPEATING: + this.conditionFormGroup.get('count').enable({emitEvent: false}); + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern'; + break; + case AlarmRuleConditionType.SIMPLE: + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + break; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html new file mode 100644 index 0000000000..e17d8c8261 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -0,0 +1,52 @@ + +
+
+
{{ 'alarm-rule.condition' | translate }}
+ +
+
+
{{ 'alarm-rule.schedule-title' | translate }}
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss new file mode 100644 index 0000000000..ac77089857 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + flex: 1; + + .tb-alarm-rule-condition { + display: flex; + flex: 1; + &-button { + --mat-outlined-button-horizontal-padding: 3px 0px 12px; + display: block; + width: 100%; + } + &-label { + display: block; + text-align: start; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts new file mode 100644 index 0000000000..17ed11d892 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { + dayOfWeekTranslations, + getAlarmScheduleRangeText, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { TimeUnit } from '@shared/models/time/time.models'; +import { + CfAlarmRuleConditionDialogComponent, + CfAlarmRuleConditionDialogData +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleExpressionType, + AlarmRuleSchedule, + AlarmRuleScheduleType +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleScheduleDialogData, + CfAlarmScheduleDialogComponent +} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-rule-condition', + templateUrl: './cf-alarm-rule-condition.component.html', + styleUrls: ['./cf-alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true, + } + ] +}) +export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + alarmRuleConditionFormGroup = this.fb.group({ + type: ['SIMPLE'], + expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required], + schedule: [null], + }); + + specText = ''; + + scheduleText = ''; + + private modelValue: AlarmRuleCondition; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private translate: TranslateService) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRuleCondition): void { + this.modelValue = value; + this.updateConditionInfo(); + } + + public conditionSet() { + return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + } + + public validate(c: UntypedFormControl) { + return this.conditionSet() ? null : { + alarmRuleCondition: { + valid: false, + }, + }; + } + + public openFilterDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmRuleConditionDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + condition: this.disabled ? this.modelValue : deepClone(this.modelValue), + arguments: this.arguments + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = {...this.modelValue, ...result}; + this.updateModel(); + this.cd.detectChanges(); + } + }); + } + + private updateConditionInfo() { + this.alarmRuleConditionFormGroup.patchValue( + { + type: this.modelValue?.type, + expression: this.modelValue?.expression, + schedule: this.modelValue?.schedule, + }, {emitEvent: false} + ); + this.updateScheduleText(); + this.updateSpecText(); + } + + private updateSpecText() { + this.specText = ''; + if (this.modelValue && this.modelValue.type) { + const type = this.modelValue.type; + switch (type) { + case AlarmRuleConditionType.SIMPLE: + break; + case AlarmRuleConditionType.DURATION: + let duringText = ''; + switch (this.modelValue.unit) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue}); + break; + } + if (this.modelValue.value.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { + attribute: `${this.modelValue.value.dynamicValueArgument}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-during', { + during: duringText + }); + } + break; + case AlarmRuleConditionType.REPEATING: + if (this.modelValue.count.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', { + attribute: `${this.modelValue.count.dynamicValueArgument}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times', + {count: this.modelValue.count.staticValue}); + } + break; + } + } + } + + private updateModel() { + this.updateConditionInfo(); + this.propagateChange(this.modelValue); + } + + public openScheduleDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmScheduleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule), + arguments: this.arguments + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue.schedule = result; + this.updateModel(); + this.cd.detectChanges(); + } + }); + } + + private updateScheduleText() { + let schedule = this.modelValue?.schedule; + this.scheduleText = ''; + if (isDefinedAndNotNull(schedule)) { + if (schedule.dynamicValueArgument) { + this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument + } else { + switch (schedule.staticValue.type) { + case AlarmRuleScheduleType.ANY_TIME: + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + for (const day of schedule.staticValue.daysOfWeek) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); + } + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn), + utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + ''; + break; + case AlarmRuleScheduleType.CUSTOM: + for (const item of schedule.staticValue.items) { + if (item.enabled) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), + utcTimestampToTimeOfDay(item.endsOn)) + ''; + } + } + break; + } + } + } + if (!this.scheduleText.length) { + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html new file mode 100644 index 0000000000..cd6b2c3796 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -0,0 +1,50 @@ + +
+ + + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { +
+
+ alarm-rule.alarm-rule-additional-info +
+ + + + +
+ } + @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { +
+
+ alarm-rule.alarm-rule-mobile-dashboard +
+ + +
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss new file mode 100644 index 0000000000..21b8a67c24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-alarm-rule-details, .tb-alarm-rule-dashboard { + padding: 4px; + &.title { + opacity: 0.7; + overflow: visible; + } + } + .tb-alarm-rule-details { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts new file mode 100644 index 0000000000..88ae1dd79d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -0,0 +1,149 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { isDefinedAndNotNull } from '@core/utils'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleDetailsDialogComponent, + AlarmRuleDetailsDialogData +} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-rule', + templateUrl: './cf-alarm-rule.component.html', + styleUrls: ['./cf-alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true, + } + ] +}) +export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + arguments: Record; + + private modelValue: AlarmRule; + + alarmRuleFormGroup = this.fb.group({ + condition: this.fb.control({}, Validators.required), + alarmDetails: [null], + dashboardId: [null] + }); + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRule): void { + this.modelValue = value; + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AlarmRuleDetailsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value, + readonly: this.disabled + } + }).afterClosed().subscribe((alarmDetails) => { + if (isDefinedAndNotNull(alarmDetails)) { + this.alarmRuleFormGroup.patchValue({alarmDetails}); + } + }); + } + + public validate(c: UntypedFormControl) { + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.alarmRuleFormGroup.value; + this.modelValue = {...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null} as AlarmRule; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss new file mode 100644 index 0000000000..14df9a3972 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + form { + width: 900px; + max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + } + + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html new file mode 100644 index 0000000000..2d12355f48 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}

+ + + {{ 'alarm-rule.static-schedule' | translate }} + {{ 'alarm-rule.dynamic-schedule' | translate }} + + +
+
+ + +
+
+ + @if (!readonly) { + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts new file mode 100644 index 0000000000..e132dd499e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleScheduleDialogData { + readonly: boolean; + alarmSchedule: AlarmRuleSchedule; + arguments: Record; +} + +@Component({ + selector: 'tb-cf-alarm-schedule-dialog', + templateUrl: './cf-alarm-schedule-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class CfAlarmScheduleDialogComponent extends DialogComponent{ + + readonly = this.data.readonly; + alarmSchedule = this.data.alarmSchedule; + arguments = this.data.arguments; + + alarmScheduleControl = this.fb.control(null); + + dynamicModeControl = this.fb.control(false); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false}); + if (this.readonly) { + this.alarmScheduleControl.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmScheduleControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html new file mode 100644 index 0000000000..7fe09d7850 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html @@ -0,0 +1,125 @@ + +
+ @if (!dynamicMode) { + + + + @for (scheduleType of alarmScheduleTypes; track scheduleType) { + {{ alarmScheduleTypeTranslate.get(scheduleType) | translate }} + } + + @if (alarmScheduleForm.get('staticValue.type').hasError('required')) { + {{ 'alarm-rule.schedule-type-required' | translate }} + } + + + @if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) { +
+
+ + + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) { +
+ + + {{ dayOfWeekTranslationsArray[day] | translate }} + + + +
+
+ + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
+
+
+
+
+
+ } + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) { +
+
+
+ + {{ dayOfWeekTranslationsArray[day] | translate }} + +
+ + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
+
+
+
+
+ +
+ } +
+
+ } + } @else { +
+ + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) { + {{ 'calculated-fields.hint.argument-name-required' | translate }} + } + +
+
+
+ } +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts new file mode 100644 index 0000000000..b1c7a4da7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -0,0 +1,322 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + CustomTimeSchedulerItem, + dayOfWeekTranslations, + getAlarmScheduleRangeText, + timeOfDayToUTCTimestamp, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { isDefined } from '@core/utils'; +import { getDefaultTimezone } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AlarmRuleSchedule, + AlarmRuleScheduleType, + AlarmRuleScheduleTypeTranslationMap +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { MatChipSelectionChange } from "@angular/material/chips"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-schedule', + templateUrl: './cf-alarm-schedule.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }] +}) +export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + dynamicMode: boolean; + + alarmScheduleForm = this.fb.group({ + staticValue: this.fb.group({ + type: [AlarmRuleScheduleType.ANY_TIME, Validators.required], + timezone: ['', Validators.required], + daysOfWeek: this.fb.control(null, Validators.required), + startsOn: this.fb.control(0, Validators.required), + endsOn: this.fb.control(0, Validators.required), + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems), + }), + dynamicValueArgument: ['', Validators.required] + }); + + alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array; + alarmScheduleType = AlarmRuleScheduleType; + alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap; + dayOfWeekTranslationsArray = dayOfWeekTranslations; + + argumentsList: Array; + + allDays = Array(7).fill(0).map((x, i) => i); + + private modelValue: AlarmRuleSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + 1 + })) as CustomTimeSchedulerItem[]; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((type) => { + const defaultTimezone = getDefaultTimezone(); + const staticValue = {...this.alarmScheduleForm.get('staticValue').value, type, items: this.defaultItems, timezone: defaultTimezone}; + this.alarmScheduleForm.get('staticValue').patchValue(staticValue, {emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false}); + this.updateValidators(type); + }); + this.alarmScheduleForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + + this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((items) => { + items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false)) + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.dynamicMode) { + const dynamicModeChanges = changes.dynamicMode; + if (!dynamicModeChanges.firstChange && dynamicModeChanges.currentValue !== dynamicModeChanges.previousValue) { + this.updateModeValidators(dynamicModeChanges.currentValue); + this.updateModel(); + } + } + } + + validateItems(control: AbstractControl): ValidationErrors | null { + const items: any[] = control.value; + if (!items || !items.length || !items.find(v => v.enabled === true)) { + return { + dayOfWeeks: true + }; + } + return null; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmScheduleForm.disable({emitEvent: false}); + } else { + this.updateModeValidators(this.dynamicMode); + } + } + + writeValue(value: AlarmRuleSchedule): void { + if (value) { + this.modelValue = value; + if (this.modelValue.dynamicValueArgument) { + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); + } else { + switch (this.modelValue.staticValue.type) { + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + daysOfWeek: this.modelValue.staticValue.daysOfWeek, + startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn), + endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn), + }, + }, {emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + if (this.modelValue?.dynamicValueArgument) { + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + }, + }, {emitEvent: false}); + } else if (this.modelValue.staticValue?.items) { + const alarmDays = []; + this.modelValue.staticValue.items + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) + .forEach((item, index) => { + this.disabledSelectedTime(item.enabled, index); + alarmDays.push({ + enabled: item.enabled, + startsOn: utcTimestampToTimeOfDay(item.startsOn), + endsOn: utcTimestampToTimeOfDay(item.endsOn) + }); + }); + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + items: alarmDays, + }, + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.staticValue.type); + } + this.updateModeValidators(this.dynamicMode); + } + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + private updateModeValidators(mode: boolean) { + if (mode) { + this.alarmScheduleForm.get('staticValue').disable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + this.alarmScheduleForm.get('staticValue').enable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false}); + this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value); + } + } + + private updateValidators(type: AlarmRuleScheduleType){ + switch (type){ + case AlarmRuleScheduleType.ANY_TIME: + this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: false}); + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value as AlarmRuleSchedule; + if (!this.dynamicMode) { + if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) { + value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn); + } + if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) { + value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn); + } + if (isDefined(value.staticValue.items)){ + value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items as CustomTimeSchedulerItem[]; + value.staticValue.items = value.staticValue.items.map((item) => { + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; + }); + } + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + + private defaultItemsScheduler(index: number): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index + 1], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatChipSelectionChange, index: number) { + const value = $event.selected; + this.disabledSelectedTime(value, index, true); + } + + private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) { + if (enable) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent}); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent}); + } + } + + getSchedulerRangeText(control: FormGroup | AbstractControl): string { + return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); + } + + get itemsSchedulerForm(): FormArray { + return this.alarmScheduleForm.get('staticValue.items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html new file mode 100644 index 0000000000..820418a6b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -0,0 +1,65 @@ + + +
+ @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) { +
+
+
+
alarm.severity
+ + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + +
+ + +
+ +
+ } +
+ + alarm-rule.add-create-alarm-rule-prompt + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss new file mode 100644 index 0000000000..c00cf6af9c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .create-alarm-rule { + border: 1px solid rgba(0, 0, 0, .12); + border-left-width: 4px; + border-radius: 4px; + padding: 8px; + min-width: 0; + } +} + +:host ::ng-deep { + .mat-mdc-form-field.severity { + .mat-mdc-form-field-infix { + width: 160px; + } + } + .button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts new file mode 100644 index 0000000000..1ffc52bb34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-create-cf-alarm-rules', + templateUrl: './create-cf-alarm-rules.component.html', + styleUrls: ['./create-cf-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true, + } + ] +}) +export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + alarmSeverityTranslationMap = alarmSeverityTranslations; + + AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; + + createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) + }); + + private usedSeverities: AlarmSeverity[] = []; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + this.createAlarmRulesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + createAlarmRulesFormArray(): UntypedFormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: Record): void { + const createAlarmRulesControls: Array = []; + if (createAlarmRules) { + Object.keys(createAlarmRules).forEach((severity) => { + const createAlarmRule = createAlarmRules[severity]; + if (severity === 'empty') { + severity = null; + } + createAlarmRulesControls.push(this.fb.group({ + severity: [severity, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + }); + } + const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + formArray.clear(); + createAlarmRulesControls.forEach(c => formArray.push(c)); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + this.updateUsedSeverities(); + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + public removeCreateAlarmRule(index: number) { + (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index); + } + + public addCreateAlarmRule() { + const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [this.getFirstUnusedSeverity(), Validators.required], + alarmRule: [null, Validators.required] + })); + this.createAlarmRulesFormGroup.updateValueAndValidity(); + if (!this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + private getFirstUnusedSeverity(): AlarmSeverity { + for (const severityKey of Object.keys(AlarmSeverity)) { + const severity = AlarmSeverity[severityKey]; + if (this.usedSeverities.indexOf(severity) === -1) { + return severity; + } + } + return null; + } + + public validate(c: UntypedFormControl) { + return (this.createAlarmRulesFormGroup.valid) ? null : { + createAlarmRules: { + valid: false, + }, + }; + } + + public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean { + const usedIndex = this.usedSeverities.indexOf(severity); + return usedIndex > -1 && usedIndex !== index; + } + + private updateUsedSeverities() { + this.usedSeverities = []; + const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + value.forEach((rule, index) => { + this.usedSeverities[index] = AlarmSeverity[rule.severity]; + }); + } + + private updateModel() { + const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + const createAlarmRules = {} as Record; + value.forEach(v => createAlarmRules[v.severity] = v.alarmRule); + this.updateUsedSeverities(); + this.propagateChange(createAlarmRules); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html new file mode 100644 index 0000000000..75bce37a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -0,0 +1,56 @@ + +
+ +

filter.complex-filter

+ +
+
+ + filter.operation.operation + + + {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} + + + + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts new file mode 100644 index 0000000000..941ce326e1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType +} from '@shared/models/query/query.models'; +import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleComplexFilterPredicateDialogData { + complexPredicate: ComplexAlarmRuleFilterPredicate; + isAdd: boolean; + valueType: EntityKeyValueType; + arguments: Record; + argumentInUse: string; +} + +@Component({ + selector: 'tb-alarm-rule-complex-filter-predicate-dialog', + templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', + providers: [], + styleUrls: [] +}) + +export class AlarmRuleComplexFilterPredicateDialogComponent extends + DialogComponent { + + complexFilterFormGroup = this.fb.group( + { + operation: [ComplexOperation.AND, [Validators.required]], + predicates: this.fb.control(null, Validators.required) + } + ); + + complexOperations = Object.keys(ComplexOperation); + complexOperationEnum = ComplexOperation; + complexOperationTranslations = complexOperationTranslationMap; + + isAdd: boolean; + + arguments = this.data.arguments; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.isAdd = this.data.isAdd; + + this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false}); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; + predicate.type = FilterPredicateType.COMPLEX; + this.dialogRef.close(predicate); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html new file mode 100644 index 0000000000..abc1dd463d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -0,0 +1,99 @@ + +
+ +

{{(data.isAdd ? 'alarm-rule.add-filter' : 'alarm-rule.edit-filter') | translate}}

+ +
+
+
+
+
{{ 'alarm-rule.general' | translate }}
+
+ + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterFormGroup.get('argument').touched && filterFormGroup.get('argument').hasError('required')) { + + warning + + } + + + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }} + + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + + + {{ 'filter.value-type-required' | translate }} + + +
+
+ +
+
+
{{ 'alarm-rule.filter' | translate }}
+ + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss new file mode 100644 index 0000000000..0ec7194f02 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-mdc-form-field.tb-value-type { + mat-select-trigger { + .mat-icon { + vertical-align: middle; + margin-right: 8px; + svg { + vertical-align: initial; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts new file mode 100644 index 0000000000..6961524bdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -0,0 +1,120 @@ + /// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + import { Component, DestroyRef, Inject } from '@angular/core'; + import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + import { Store } from '@ngrx/store'; + import { AppState } from '@core/core.state'; + import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + import { Router } from '@angular/router'; + import { DialogComponent } from '@app/shared/components/dialog.component'; + import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypesMap + } from '@shared/models/query/query.models'; + import { DialogService } from '@core/services/dialog.service'; + import { TranslateService } from '@ngx-translate/core'; + import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; + import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + import { FormControlsFrom } from "@shared/models/tenant.model"; + import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + + export interface AlarmRuleFilterDialogData { + filter: AlarmRuleFilter; + isAdd: boolean; + arguments: Record; + usedArguments: Array; +} + +@Component({ + selector: 'tb-alarm-rule-filter-dialog', + templateUrl: './alarm-rule-filter-dialog.component.html', + providers: [], + styleUrls: ['./alarm-rule-filter-dialog.component.scss'] +}) +export class AlarmRuleFilterDialogComponent extends DialogComponent { + + filterFormGroup: FormGroup>; + + entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); + + entityKeyValueTypeEnum = EntityKeyValueType; + + entityKeyValueTypes = entityKeyValueTypesMap; + + complexOperationTranslationMap = complexOperationTranslationMap; + + ComplexOperation = ComplexOperation; + + arguments = this.data.arguments; + argumentsList: Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, + public dialogRef: MatDialogRef, + private dialogs: DialogService, + private translate: TranslateService, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + super(store, router, dialogRef); + + this.argumentsList = this.arguments ? Object.keys(this.arguments) : []; + + this.filterFormGroup = this.fb.group( + { + argument: [this.data.filter.argument, [Validators.required]], + valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]], + predicates: [this.data.filter.predicates, [Validators.required]], + operation: [this.data.filter.operation ?? ComplexOperation.AND] + } + ); + this.filterFormGroup.get('valueType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((valueType: EntityKeyValueType) => { + const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType; + const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value; + if (prevValueType && prevValueType !== valueType) { + if (predicates && predicates.length) { + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), + this.translate.instant('filter.key-value-type-change-message')).subscribe( + (result) => { + if (result) { + this.filterFormGroup.get('predicates').setValue([]); + } else { + this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false}); + } + } + ); + } + } + }); + } + + argumentInUse(argument: string): boolean { + return this.data.usedArguments.includes(argument); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html new file mode 100644 index 0000000000..3f588a3d70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html @@ -0,0 +1,79 @@ + +
+
+ +
+ + +   +
+
+ +
+ @for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) { +
+
+ @if ($index) { +
+ {{ complexOperationTranslationMap.get(operation) | translate }} +
+ } +
+
+
+
{{ filterControl.value?.argument }}
+
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}
+ + +
+
+ @if (index) { + + } +
+ } + + filter.no-key-filters + +
+
+ + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss new file mode 100644 index 0000000000..8e74f373f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .filter-title { + padding: 12px 0; + font-size: 14px; + font-weight: 500; + } + .filter-list { + overflow: auto; + max-height: 300px; + .no-data-found { + height: 50px; + } + + &-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + background-color: white; + } + &-label { + font-weight: 500; + color: #00695C; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba(#00695C, 0.32); + background-color: rgba(#00695C, 0.04); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts new file mode 100644 index 0000000000..11e2728659 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable } from 'rxjs'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone } from '@core/utils'; +import { + AlarmRuleFilterDialogComponent, + AlarmRuleFilterDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-list', + templateUrl: './alarm-rule-filter-list.component.html', + styleUrls: ['./alarm-rule-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator { + + @Input() + arguments: Record; + + @Input() + operation: ComplexOperation = ComplexOperation.AND; + + filterListFormGroup = this.fb.group({ + filters: this.fb.array([]) + }); + + complexOperationTranslationMap = complexOperationTranslationMap; + FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + + get filtersFormArray(): FormArray { + return this.filterListFormGroup.get('filters') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterListFormGroup.valid && this.filterListFormGroup.get('filters').value?.length ? null : { + filterList: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(filters: Array): void { + const keyFilterControls: Array = []; + if (filters) { + for (const filter of filters) { + keyFilterControls.push(this.fb.control(filter, [Validators.required])); + } + } + this.filtersFormArray.clear(); + keyFilterControls.forEach(c => this.filtersFormArray.push(c)); + } + + public removeFilter(index: number) { + (this.filterListFormGroup.get('filters') as FormArray).removeAt(index); + } + + public addFilter() { + const filtersFormArray = this.filterListFormGroup.get('filters') as FormArray; + this.openFilterDialog(null).subscribe(result => { + if (result) { + filtersFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + public editFilter(index: number) { + const filter: AlarmRuleFilter = + (this.filterListFormGroup.get('filters') as FormArray).at(index).value; + this.openFilterDialog(filter).subscribe(result => { + if (result) { + (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result); + } + }); + } + + private openFilterDialog(filter?: AlarmRuleFilter): Observable { + const isAdd = !filter; + if (isAdd) { + filter = { + argument: null, + valueType: EntityKeyValueType.STRING, + operation: ComplexOperation.AND, + predicates: [] + }; + } + return this.dialog.open(AlarmRuleFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filter: filter ? deepClone(filter) : null, + isAdd, + arguments: this.arguments, + usedArguments: this.getUsedArguments + } + }).afterClosed(); + } + + get getUsedArguments(): Array { + const filters = this.filterListFormGroup.get('filters').value as Array; + return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : []; + } + + private updateModel() { + const filters = this.filterListFormGroup.value.filters as Array; + this.propagateChange(filters); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html new file mode 100644 index 0000000000..b7450262f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -0,0 +1,86 @@ + +
+
+
+
+
+ + + +
+   +
+
+ +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
+ @if (index) { +
+ {{ complexOperationTranslations.get(operation) | translate }} +
+ } +
+
+ + + +
+
+
+ } + + filter.no-filters + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss new file mode 100644 index 0000000000..054d0e5281 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .predicate-list { + .no-data-found { + height: 50px; + } + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-weight: 500; + color: #00695C; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba(#00695C, 0.32); + background-color: rgba(#00695C, 0.04); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts new file mode 100644 index 0000000000..ecb52944b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { + BooleanOperation, + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypeToFilterPredicateType, + FilterPredicateType, + NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { map } from 'rxjs/operators'; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { + AlarmRuleFilterPredicate, + AlarmRulePredicateInfo, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-list', + templateUrl: './alarm-rule-filter-predicate-list.component.html', + styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() operation: ComplexOperation = ComplexOperation.AND; + + @Input() arguments: Record; + + @Input() argumentInUse: string; + + filterListFormGroup = this.fb.group({ + predicates: this.fb.array([]) + }); + + valueTypeEnum = EntityKeyValueType; + + complexOperationTranslations = complexOperationTranslationMap; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + get predicatesFormArray(): FormArray { + return this.filterListFormGroup.get('predicates') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + validate(control: AbstractControl): ValidationErrors | null { + return this.filterListFormGroup.valid ? null : { + filterList: {valid: false} + }; + } + + writeValue(predicates: Array): void { + const predicateControls: Array = []; + if (predicates) { + for (const predicate of predicates) { + predicateControls.push(this.fb.control(predicate, [Validators.required])); + } + } + this.predicatesFormArray.clear(); + predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate)); + } + + public removePredicate(index: number) { + this.predicatesFormArray.removeAt(index); + } + + public addPredicate(complex: boolean) { + const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; + const predicate = this.createDefaultFilterPredicate(this.valueType, complex); + let observable: Observable; + if (complex) { + observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate); + } else { + observable = of(predicate); + } + observable.subscribe((result) => { + if (result) { + predicatesFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate { + const predicate = { + type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) + } as AlarmRuleFilterPredicate; + switch (predicate.type) { + case FilterPredicateType.STRING: + predicate.operation = StringOperation.STARTS_WITH; + predicate.value = { + staticValue: '' + }; + predicate.ignoreCase = false; + break; + case FilterPredicateType.NUMERIC: + predicate.operation = NumericOperation.EQUAL; + predicate.value = { + staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 + }; + break; + case FilterPredicateType.BOOLEAN: + predicate.operation = BooleanOperation.EQUAL; + predicate.value = { + staticValue: false + }; + break; + case FilterPredicateType.COMPLEX: + predicate.operation = ComplexOperation.AND; + predicate.predicates = []; + break; + } + return predicate; + } + + private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable { + return this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: predicate as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: true, + arguments: this.arguments, + argumentInUse: this.argumentInUse + } + }).afterClosed().pipe( + map(result => result) + ); + } + + private updateModel() { + this.propagateChange(this.filterListFormGroup.get('predicates').value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html new file mode 100644 index 0000000000..10294e851a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html @@ -0,0 +1,73 @@ + +
+
+ + + {{'alarm-rule.static' | translate}} + {{'alarm-rule.dynamic' | translate}} + + + @if (!dynamicModeControl.value) { + @switch (valueType) { + @case (valueTypeEnum.STRING) { + + + + } + @case (valueTypeEnum.NUMERIC) { + + + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('staticValue').value ? 'value.true' : 'value.false') | translate }} + + } + @case (valueTypeEnum.DATE_TIME) { + + } + } + } @else { + + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) { + + warning + + } + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts new file mode 100644 index 0000000000..036083c826 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -0,0 +1,155 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { EntityKeyValueType } from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; +import { FormControlsFrom } from "@shared/models/tenant.model"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-value', + templateUrl: './alarm-rule-filter-predicate-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { + + @Input() + arguments: Record; + + @Input() + valueType: EntityKeyValueType; + + @Input() + argumentInUse: string; + + valueTypeEnum = EntityKeyValueType; + + filterPredicateValueFormGroup: FormGroup>>; + + dynamicModeControl = this.fb.control(false); + + argumentsList: Array; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + let defaultValue: string | number | boolean; + let defaultValueValidators: ValidatorFn[]; + switch (this.valueType) { + case EntityKeyValueType.STRING: + defaultValue = ''; + defaultValueValidators = []; + break; + case EntityKeyValueType.NUMERIC: + defaultValue = 0; + defaultValueValidators = [Validators.required]; + break; + case EntityKeyValueType.BOOLEAN: + defaultValue = false; + defaultValueValidators = []; + break; + case EntityKeyValueType.DATE_TIME: + defaultValue = Date.now(); + defaultValueValidators = [Validators.required]; + break; + } + this.filterPredicateValueFormGroup = this.fb.group({ + staticValue: [defaultValue, defaultValueValidators], + dynamicValueArgument: ['', Validators.required] + }); + this.filterPredicateValueFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.dynamicModeControl.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => this.updateValueModeValidators(value)); + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterPredicateValueFormGroup.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } else { + this.filterPredicateValueFormGroup.enable({emitEvent: false}); + this.dynamicModeControl.enable({emitEvent: false}); + this.updateValueModeValidators(this.dynamicModeControl.value); + } + } + + private updateValueModeValidators(isDynamicMode: boolean): void { + if (isDynamicMode) { + this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); + } else { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('staticValue').enable(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateValueFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + + writeValue(predicateValue: AlarmRuleValue): void { + this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange(this.filterPredicateValueFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html new file mode 100644 index 0000000000..d73c222f9d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -0,0 +1,80 @@ + +
+
+ @switch (type) { + @case (filterPredicateType.STRING) { +
+ + + + {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} + + + + + {{ 'alarm-rule.ignore-case' | translate }} + +
+ } + @case (filterPredicateType.NUMERIC) { +
+ + + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + + +
+ } + @case (filterPredicateType.BOOLEAN) { +
+ + + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + + +
+ } + @case (filterPredicateType.COMPLEX) { +
+ +
+ } + } + @if (type !== filterPredicateType.COMPLEX) { + + + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts new file mode 100644 index 0000000000..b508be0c79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -0,0 +1,162 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { + BooleanOperation, + booleanOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType, + NumericOperation, + numericOperationTranslationMap, + StringOperation, + stringOperationTranslationMap +} from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { MatDialog } from "@angular/material/dialog"; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate', + templateUrl: './alarm-rule-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator { + + @Input({ transform: booleanAttribute }) + disabled: boolean; + + @Input() + valueType: EntityKeyValueType; + + @Input() + arguments: Record; + + @Input() + argumentInUse: string; + + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [] + }); + + type: FilterPredicateType; + + filterPredicateType = FilterPredicateType; + + stringOperations = Object.keys(StringOperation); + stringOperation = StringOperation; + stringOperationTranslationMap = stringOperationTranslationMap; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterPredicateFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateFormGroup.valid ? null : { + filterPredicate: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.filterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: AlarmRuleFilterPredicate): void { + this.type = predicate.type; + this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value}); + } + + public openComplexFilterDialog() { + this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: this.filterPredicateFormGroup.value as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: false, + arguments: this.arguments, + argumentInUse: this.argumentInUse, + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html new file mode 100644 index 0000000000..cb390b6c0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html @@ -0,0 +1,23 @@ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss new file mode 100644 index 0000000000..a059bb4564 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + text-overflow: ellipsis; + overflow: hidden; + .tb-filter-text { + overflow-y: auto; + text-align: start; + &.required { + color: #f44336; + padding: 0; + } + &.nowrap { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .tb-filter-text { + line-height: 1.8em; + span { + display: inline-block; + vertical-align: middle; + line-height: 1.4em; + } + .tb-filter-predicate { + padding-right: 4px; + padding-left: 4px; + } + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { + font-weight: bold; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + } + .tb-filter-entity-key, .tb-filter-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + } + .tb-filter-dynamic-source { + } + .tb-filter-entity-key { + color: #305680; + } + .tb-filter-value { + color: #ff5722; + } + .tb-filter-simple-operation { + font-size: 0.9em; + } + .tb-filter-complex-operation { + font-weight: 400; + font-style: italic; + } + .tb-filter-dynamic-value { + .tb-filter-dynamic-source, .tb-filter-value { + color: #0c959c; + } + } + .tb-filter-bracket { + .tb-left-bracket, .tb-right-bracket { + font-size: 1.2em; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts new file mode 100644 index 0000000000..498b2a0a81 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -0,0 +1,192 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { + booleanOperationTranslationMap, + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType, + numericOperationTranslationMap, + stringOperationTranslationMap +} from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { + AlarmRuleExpression, + AlarmRuleExpressionType, + AlarmRuleFilter, + AlarmRuleFilterPredicate, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-alarm-rule-filter-text', + templateUrl: './alarm-rule-filter-text.component.html', + styleUrls: ['./alarm-rule-filter-text.component.scss'], + providers: [] +}) +export class AlarmRuleFilterTextComponent { + + @Input() + @coerceBoolean() + required = false; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + @Input() + @coerceBoolean() + nowrap = false; + + @Input() + arguments: Record; + + private alarmRuleExpressionValue: AlarmRuleExpression; + get alarmRuleExpression(): AlarmRuleExpression { + return this.alarmRuleExpressionValue; + } + + @Input() + set alarmRuleExpression(value: AlarmRuleExpression) { + if (value !== this.alarmRuleExpressionValue) { + this.alarmRuleExpressionValue = value; + this.updateFilterText(value); + } + }; + + private specTextValue: string; + get specText(): string { + return this.specTextValue; + } + @Input() + set specText(value: string) { + if (value !== this.specTextValue) { + this.specTextValue = value; + this.updateFilterText(this.alarmRuleExpression); + } + } + + isRequired = false; + + public filterText: string; + + constructor(private translate: TranslateService, + private datePipe: DatePipe) { + } + + private updateFilterText(value: AlarmRuleExpression) { + this.isRequired = false; + if (value && (value.expression || value.filters)) { + if (value.type === AlarmRuleExpressionType.SIMPLE) { + this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); + } else { + this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')'; + } + if (this.specText?.length) { + this.filterText = this.specText + ': ' + this.filterText; + } + } else { + if (this.required) { + this.filterText = this.addFilterPrompt; + this.isRequired = true; + } else { + this.filterText = this.noFilterText; + } + } + } + + private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, operation: ComplexOperation): string { + const filtersText = keyFilters.map(keyFilter => + this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates)); + let result: string; + if (filtersText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(operation)); + result = filtersText.join(' ' + operationText + ' '); + } else { + result = filtersText[0]; + } + return result; + } + + private filterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: AlarmRuleFilter, + keyFilterPredicates: AlarmRuleFilterPredicate[], + complexOperation?: ComplexOperation): string { + const key = keyFilter.argument; + const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); + + const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; + const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; + return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); + } else { + let operation: string; + let value: string; + const val = keyFilterPredicate.value; + const dynamicValue = val?.dynamicValueArgument?.length; + if (dynamicValue) { + value = '' + val?.dynamicValueArgument + ''; + } + switch (keyFilterPredicate.type) { + case FilterPredicateType.STRING: + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.staticValue}'`; + } + break; + case FilterPredicateType.NUMERIC: + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.staticValue + ''; + } + } + break; + case FilterPredicateType.BOOLEAN: + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); + } + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}` + } + }); + if (predicates.length > 1) { + return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')'; + } else { + return predicates.toString(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts new file mode 100644 index 0000000000..5e3a854aa2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldDialogComponent +} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; +import { + CalculatedFieldScriptTestDialogComponent +} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; +import { + CalculatedFieldTestArgumentsComponent +} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; +import { + EntityDebugSettingsButtonComponent +} from '@home/components/entity/debug/entity-debug-settings-button.component'; +import { + GeofencingConfigurationModule +} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module'; +import { + SimpleConfigurationModule +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; +import { + PropagationConfigurationModule +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; +import { + RelatedEntitiesAggregationComponentModule +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module'; + +@NgModule({ + declarations: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestArgumentsComponent, + ], + imports: [ + CommonModule, + SharedModule, + GeofencingConfigurationModule, + EntityDebugSettingsButtonComponent, + SimpleConfigurationModule, + PropagationConfigurationModule, + RelatedEntitiesAggregationComponentModule, + ], + exports: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + ] +}) +export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 5f4b894448..0ac64b428f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -40,10 +40,12 @@ import { ArgumentType, CalculatedField, CalculatedFieldEventArguments, + CalculatedFieldScriptConfiguration, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogComponent, @@ -57,6 +59,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -75,8 +78,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '300px'); + const expressionColumn = new EntityTableColumn('expression', 'calculated-fields.expression', '250px'); expressionColumn.sortable = false; expressionColumn.cellContentFunction = entity => { const expressionLabel = this.getExpressionLabel(entity); @@ -121,8 +126,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%')); - this.columns.push(new EntityTableColumn('type', 'common.type', '70px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)))); + this.columns.push(new EntityTableColumn('name', 'common.name', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.columns.push(expressionColumn); this.cellActionDescriptors.push( @@ -156,11 +162,13 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { @@ -212,6 +220,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { - const type = calculatedField.configuration.arguments[key].refEntityKey.type; - acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) - ? { ...argumentsObj[key], type } - : type === ArgumentType.Rolling ? { values: [], type } : { value: '', type, ts: new Date().getTime() }; - return acc; - }, {}); - return this.dialog.open(CalculatedFieldScriptTestDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression: calculatedField.configuration.expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), - openCalculatedFieldEdit - } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => { - if (openCalculatedFieldEdit) { - this.editCalculatedField({ entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression } }, true) + if ( + calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true) + ) { + const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? {...argumentsObj[key], type} + : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; + return acc; + }, {}); + return this.dialog.open(CalculatedFieldScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), + openCalculatedFieldEdit } - }), - ); + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => { + if (openCalculatedFieldEdit) { + this.editCalculatedField({ + entityId: this.entityId, ...calculatedField, + configuration: {...calculatedField.configuration, expression} as any + }, true) + } + }), + ); + } else { + return of(null); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index e10c4b301e..ee3d013907 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -35,6 +35,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; @Component({ selector: 'tb-calculated-fields-table', @@ -50,6 +51,7 @@ export class CalculatedFieldsTableComponent { active = input(); entityId = input(); entityName = input(); + ownerId = input(); calculatedFieldsTableConfig: CalculatedFieldsTableConfig; @@ -62,6 +64,7 @@ export class CalculatedFieldsTableComponent { private renderer: Renderer2, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, private destroyRef: DestroyRef) { effect(() => { @@ -76,8 +79,10 @@ export class CalculatedFieldsTableComponent { this.destroyRef, this.renderer, this.entityName(), + this.ownerId(), this.importExportService, this.entityDebugSettingsService, + this.utilsService, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html similarity index 55% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index 92855d882f..c7eeaa253b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -15,75 +15,55 @@ limitations under the License. --> -
-
-
{{ 'calculated-fields.argument-settings' | translate }}
-
-
-
{{ 'calculated-fields.argument-name' | translate }}
- - - @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { - - warning - - } - +
+
{{ 'calculated-fields.argument-settings' | translate }}
+
+ @if (hint) { +
+ {{ hint | translate }}
- -
-
{{ 'entity.entity-type' | translate }}
- - - @for (type of argumentEntityTypes; track type) { - {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } +
+ @if (!isOutputKey) { + + } + + @if (!hiddenEntityTypes) { +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + @if (argumentType.touched && argumentType.hasError('required')) { + + warning + } - - -
+ +
+ } @if (ArgumentEntityTypeParamsMap.has(entityType)) {
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
+ @if (isOutputKey) { + + } @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
-
{{ 'calculated-fields.default-value' | translate }}
+
{{ 'calculated-fields.default-value' | translate }}
+ @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { + + warning + + }
} @else { @@ -191,7 +190,7 @@ }
-
+
+ + +
+
{{ label | translate }}
+ + + @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { + + warning + + } + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss similarity index 80% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss index 773489ee60..f3e13b1f19 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss @@ -15,22 +15,9 @@ */ @use '../../../../../../../scss/constants' as constants; -$panel-width: 520px; - :host { - display: flex; - width: $panel-width; - max-width: 100%; - max-height: 100vh; - - .fixed-title-width { - @media #{constants.$mat-xs} { - min-width: 120px; - } - } - .limit-field-row { - @media screen and (max-width: $panel-width) { + @media screen and (max-width: 520px) { display: flex; flex-direction: column; @@ -40,6 +27,10 @@ $panel-width: 520px; } } } + + .tb-primary-fill { + overflow: visible; + } } :host ::ng-deep { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts similarity index 69% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 8ccaa4fe49..4e8abbe841 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + Input, + OnInit, + output, + ViewChild +} from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; @@ -25,10 +34,9 @@ import { ArgumentType, ArgumentTypeTranslations, CalculatedFieldArgumentValue, - CalculatedFieldType, getCalculatedFieldCurrentEntityFilter } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DatasourceType } from '@shared/models/widget.models'; @@ -43,11 +51,12 @@ import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { TenantId } from '@shared/models/id/tenant-id'; @Component({ selector: 'tb-calculated-field-argument-panel', templateUrl: './calculated-field-argument-panel.component.html', - styleUrls: ['./calculated-field-argument-panel.component.scss'] + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss'] }) export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @@ -56,22 +65,28 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() calculatedFieldType: CalculatedFieldType; + @Input() ownerId: EntityId; + @Input() isScript: boolean; @Input() usedArgumentNames: string[]; + @Input() isOutputKey = false; + @Input() hiddenEntityTypes = false; + @Input() defaultValueRequired = false; + @Input() hint: string; + @Input() predefinedEntityFilter: EntityFilter; + @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; argumentsDataApplied = output(); + argumentType = this.fb.control(ArgumentEntityType.Current, Validators.required); + readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], - refEntityId: this.fb.group({ - entityType: [ArgumentEntityType.Current], - id: [''] - }), + refEntityId: [null], refEntityKey: this.fb.group({ type: [ArgumentType.LatestTelemetry, [Validators.required]], key: ['', [Validators.pattern(oneSpaceInsideRegex)]], @@ -86,7 +101,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); - readonly argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; @@ -103,20 +117,16 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private fb: FormBuilder, private cd: ChangeDetectorRef, private popover: TbPopoverComponent, - private store: Store + private store: Store, + private destroyRef: DestroyRef ) { this.observeEntityFilterChanges(); - this.observeEntityTypeChanges(); + this.observeArgumentTypeChanges(); this.observeEntityKeyChanges(); - this.observeUpdatePosition(); } get entityType(): ArgumentEntityType { - return this.argumentFormGroup.get('refEntityId').get('entityType').value; - } - - get refEntityIdFormGroup(): FormGroup { - return this.argumentFormGroup.get('refEntityId') as FormGroup; + return this.argumentType.value; } get refEntityKeyFormGroup(): FormGroup { @@ -130,14 +140,23 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } ngOnInit(): void { + this.updatedArgumentType(); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); - this.updateEntityFilter(this.argument.refEntityId?.entityType, true); + this.updateEntityFilter(this.entityType, true); + this.updatedRefEntityIdState(this.entityType, false); this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.setInitialEntityKeyType(); + this.setInitialEntityType(); + this.setWatchKeyChange(); + + if (this.defaultValueRequired) { + this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); + this.argumentFormGroup.get('defaultValue').updateValueAndValidity({onlySelf: true}); + } this.argumentTypes = Object.values(ArgumentType) - .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); + .filter(type => type !== ArgumentType.Rolling || this.isScript); } ngAfterViewInit(): void { @@ -147,12 +166,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } saveArgument(): void { - const { refEntityId, ...restConfig } = this.argumentFormGroup.value; - const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; - if (refEntityId.entityType === ArgumentEntityType.Tenant) { - refEntityId.id = this.tenantId; + const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue; + if (this.entityType === ArgumentEntityType.Owner) { + value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner; + } else if (this.entityType === ArgumentEntityType.Tenant) { + value.refEntityId = new TenantId(this.tenantId) as any; } - if (refEntityId.entityType !== ArgumentEntityType.Current && refEntityId.entityType !== ArgumentEntityType.Tenant) { + if (this.entityType !== ArgumentEntityType.Current && this.entityType !== ArgumentEntityType.Tenant) { value.entityName = this.entityNameSubject.value; } if (value.defaultValue) { @@ -166,6 +186,16 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.popover.hide(); } + private updatedArgumentType(): void { + let argumentType = ArgumentEntityType.Current; + if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + argumentType = ArgumentEntityType.Owner; + } else if (this.argument.refEntityId?.entityType) { + argumentType = this.argument.refEntityId.entityType; + } + this.argumentType.setValue(argumentType, {emitEvent: false}); + } + private toggleByEntityKeyType(type: ArgumentType): void { const isAttribute = type === ArgumentType.Attribute; const isRolling = type === ArgumentType.Rolling; @@ -181,6 +211,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI case ArgumentEntityType.Current: entityFilter = this.currentEntityFilter; break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; case ArgumentEntityType.Tenant: entityFilter = { type: AliasFilterType.singleEntity, @@ -198,6 +234,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } if (!onInit) { this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + } else if (this.predefinedEntityFilter) { + entityFilter = this.predefinedEntityFilter; } this.entityFilter = entityFilter; this.cd.markForCheck(); @@ -205,26 +243,21 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private observeEntityFilterChanges(): void { merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, + this.argumentType.valueChanges, this.refEntityKeyFormGroup.get('type').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), + this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)), this.refEntityKeyFormGroup.get('scope').valueChanges, ) .pipe(debounceTime(50), takeUntilDestroyed()) .subscribe(() => this.updateEntityFilter(this.entityType)); } - private observeEntityTypeChanges(): void { - this.refEntityIdFormGroup.get('entityType').valueChanges + private observeArgumentTypeChanges(): void { + this.argumentType.valueChanges .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { - this.argumentFormGroup.get('refEntityId').get('id').setValue(''); - const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; - this.argumentFormGroup.get('refEntityId') - .get('id')[isEntityWithId ? 'enable' : 'disable'](); - if (!isEntityWithId) { - this.entityNameSubject.next(null); - } + this.argumentFormGroup.get('refEntityId').setValue(null); + this.updatedRefEntityIdState(type); if (!this.enableAttributeScopeSelection) { this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); } @@ -247,29 +280,45 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } private setInitialEntityKeyType(): void { - if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) { + if (!this.isScript && this.argument.refEntityKey?.type === ArgumentType.Rolling) { const typeControl = this.argumentFormGroup.get('refEntityKey').get('type'); typeControl.setValue(null); typeControl.markAsTouched(); } } + private setInitialEntityType() { + if (!this.argumentEntityTypes.includes(this.entityType)) { + this.argumentType.setValue(null); + this.argumentType.markAsTouched(); + } + } + + private setWatchKeyChange(): void { + if (this.isOutputKey) { + this.refEntityKeyFormGroup.get('key').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((key) => { + if (this.argumentFormGroup.get('argumentName').pristine) { + this.argumentFormGroup.get('argumentName').setValue(key); + } + }); + } + } + private forbiddenArgumentNameValidator(): ValidatorFn { return (control: FormControl) => { const trimmedValue = control.value.trim().toLowerCase(); - const forbiddenArgumentNames = ['ctx', 'e', 'pi']; + const forbiddenArgumentNames = ['ctx', 'e', 'pi', 'propagationCtx']; return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; }; } - private observeUpdatePosition(): void { - merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, - this.refEntityKeyFormGroup.get('type').valueChanges, - this.argumentFormGroup.get('timeWindow').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), - ) - .pipe(delay(50), takeUntilDestroyed()) - .subscribe(() => this.popover.updatePosition()); + private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void { + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type); + this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']({emitEvent}); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html similarity index 88% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 8cf040538c..15d2f4b09b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -21,7 +21,7 @@ [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> -
{{ 'common.name' | translate }}
+
{{ argumentNameColumn | translate }}
@@ -29,7 +29,7 @@ @@ -37,13 +37,15 @@ - + {{ 'entity.entity-type' | translate }}
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) { {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} } @else if (argument.refEntityId?.id) { {{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }} } @else { @@ -88,7 +90,7 @@ -
+
- - @if (fieldFormGroup.get('type').value !== CalculatedFieldType.GEOFENCING) { -
-
{{ 'calculated-fields.arguments' | translate }}
- -
-
-
- {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} -
- - -
-
- @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { - - @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { - {{ 'calculated-fields.hint.expression-required' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { - {{ 'calculated-fields.hint.expression-invalid' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { - {{ 'calculated-fields.hint.expression-max-length' | translate }} - } - - } @else { - {{ 'calculated-fields.hint.expression' | translate }} - } -
-
- -
{{ 'api-usage.tbel' | translate }}
- -
-
- -
-
-
- } @else { -
-
- {{ 'calculated-fields.entity-coordinates' | translate }} -
-
- - -
-
- -
-
- {{ 'calculated-fields.geofencing-zone-groups' | translate }} -
- -
- -
- {{ 'calculated-fields.zone-group-refresh-interval' | translate }} -
-
-
- - -
-
-
+ @switch (fieldFormGroup.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + } -
-
{{ 'calculated-fields.output' | translate }}
-
- - {{ 'calculated-fields.output-type' | translate }} - - @for (type of outputTypes; track type) { - {{ OutputTypeTranslations.get(type) | translate}} - } - - - @if (outputFormGroup.get('type').value === OutputType.Attribute - && (data.entityId.entityType === EntityType.DEVICE || data.entityId.entityType === EntityType.DEVICE_PROFILE)) { - - {{ 'calculated-fields.attribute-scope' | translate }} - - - {{ 'calculated-fields.server-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} - - - - } -
- @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { -
- - - {{ (outputFormGroup.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate }} - - - @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { - - @if (outputFormGroup.get('name').hasError('required')) { - {{ 'common.hint.key-required' | translate }} - } @else if (outputFormGroup.get('name').hasError('pattern')) { - {{ 'common.hint.key-pattern' | translate }} - } @else if (outputFormGroup.get('name').hasError('maxlength')) { - {{ 'common.hint.key-max-length' | translate }} - } - - } - - - {{ 'calculated-fields.decimals-by-default' | translate }} - - @if (outputFormGroup.get('decimalsByDefault').errors && outputFormGroup.get('decimalsByDefault').touched) { - {{ 'calculated-fields.hint.decimals-range' | translate }} - } - -
-
- -
- calculated-fields.use-latest-timestamp -
-
-
- } -
-
+ @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @default { + + } + }
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 e192e3ccc0..5e83c1821c 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 @@ -17,6 +17,11 @@ .calculated-field-dialog-container { width: 869px; max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mdc-outlined-text-field-container-shape: 6px; + --mat-form-field-trailing-icon-color: rgba(0, 0, 0, 0.56); } .tbel-script-lang-chip { 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 8cca16d4ef..17c550dcdf 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 @@ -18,36 +18,25 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { - ArgumentEntityType, CalculatedField, CalculatedFieldConfiguration, - calculatedFieldDefaultScript, - CalculatedFieldGeofencing, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations, - getCalculatedFieldArgumentsEditorCompleter, - getCalculatedFieldArgumentsHighlights, - getCalculatedFieldCurrentEntityFilter, - OutputType, - OutputTypeTranslations + CalculatedFieldTypeTranslations } from '@shared/models/calculated-field.models'; -import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; -import { map, startWith, switchMap } from 'rxjs/operators'; +import { pairwise, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { EntityFilter } from '@shared/models/query/query.models'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { deepTrim } from '@core/utils'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -55,6 +44,7 @@ export interface CalculatedFieldDialogData { entityId: EntityId; tenantId: string; entityName?: string; + ownerId: EntityId; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; getTestScriptDialogFn: CalculatedFieldTestScriptFn; isDirty?: boolean; @@ -68,70 +58,22 @@ export interface CalculatedFieldDialogData { }) export class CalculatedFieldDialogComponent extends DialogComponent { - readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; - fieldFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], type: [CalculatedFieldType.SIMPLE], debugSettings: [], - configuration: this.fb.group({ - entityCoordinates: this.fb.group({ - latitudeKeyName: [null, [Validators.required]], - longitudeKeyName: [null, [Validators.required]], - }), - arguments: this.fb.control({}), - zoneGroups: this.fb.control({}), - scheduledUpdateEnabled: [true], - scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], - expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], - expressionSCRIPT: [calculatedFieldDefaultScript], - output: this.fb.group({ - name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], - scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], - type: [OutputType.Timeseries], - decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], - }), - useLatestTs: [false] - }), + configuration: this.fb.control({} as CalculatedFieldConfiguration), }); - functionArgs$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) - ); - - argumentsEditorCompleter$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj)) - ); - - argumentsHighlightRules$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) - ); - additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; - currentEntityFilter: EntityFilter; - - isRelatedEntity: boolean; - - readonly OutputTypeTranslations = OutputTypeTranslations; - readonly OutputType = OutputType; - readonly AttributeScope = AttributeScope; readonly EntityType = EntityType; readonly CalculatedFieldType = CalculatedFieldType; - readonly ScriptLanguage = ScriptLanguage; - readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[]; - readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[]; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; - readonly DataKeyType = DataKeyType; constructor(protected store: Store, protected router: Router, @@ -142,49 +84,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent { const calculatedFieldId = this.data.value?.id?.id; - let testScriptDialogResult$: Observable; - if (calculatedFieldId) { - testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) .pipe( switchMap(event => { const args = event?.arguments ? JSON.parse(event.arguments) : null; @@ -212,114 +115,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); } private applyDialogData(): void { - const { configuration = {}, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; - const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; - const updatedConfig = { ...restConfig , ['expression'+type]: expression }; - this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, debugSettings, ...value }, {emitEvent: false}); - } - - private observeTypeChanges(): void { - this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value); - this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); - - this.outputFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleScopeByOutputType(type)); - this.fieldFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleKeyByCalculatedFieldType(type)); - } - - private observeZoneChanges(): void { - this.configFormGroup.get('zoneGroups').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe((zoneGroups: CalculatedFieldGeofencing) => - this.checkRelatedEntity(zoneGroups) - ); - this.checkRelatedEntity(this.configFormGroup.get('zoneGroups').value); - } - - private observeScheduledUpdateEnabled(): void { - this.configFormGroup.get('scheduledUpdateEnabled').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe((value: boolean) => - this.checkScheduledUpdateEnabled(value) - ); - this.checkScheduledUpdateEnabled(this.configFormGroup.get('scheduledUpdateEnabled').value); - } - - private checkScheduledUpdateEnabled(value: boolean) { - if (value) { - this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false}); - } else { - this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false}); - } - } - - private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) { - this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); - } - - private toggleScopeByOutputType(type: OutputType): void { - if (type === OutputType.Attribute) { - this.outputFormGroup.get('scope').enable({emitEvent: false}); - } else { - this.outputFormGroup.get('scope').disable({emitEvent: false}); - } - if (this.fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - if (type === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } - } - - private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void { - if (type === CalculatedFieldType.GEOFENCING) { - this.configFormGroup.get('entityCoordinates').enable({emitEvent: false}); - this.configFormGroup.get('zoneGroups').enable({emitEvent: false}); - this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false}); - - this.outputFormGroup.get('name').disable({emitEvent: false}); - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false}); - this.configFormGroup.get('arguments').disable({emitEvent: false}); - } else { - this.configFormGroup.get('entityCoordinates').disable({emitEvent: false}); - this.configFormGroup.get('zoneGroups').disable({emitEvent: false}); - this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false}); - - if (type === CalculatedFieldType.SIMPLE) { - this.outputFormGroup.get('name').enable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').enable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false}); - if (this.outputFormGroup.get('type').value === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.outputFormGroup.get('name').disable({emitEvent: false}); - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false}); - } - } + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); + setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } private observeIsLoading(): void { @@ -328,12 +131,22 @@ export class CalculatedFieldDialogComponent extends DialogComponent { + if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || + ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { + this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html new file mode 100644 index 0000000000..250d6bd120 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -0,0 +1,268 @@ + +
+
{{ 'calculated-fields.geofencing-zone-groups-settings' | translate }}
+
+
+
{{ 'calculated-fields.name' | translate }}
+ + + @if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('pattern')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) { + + warning + + } + +
+ +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + +
+ @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
+
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
+ +
+ } +
+ +
+ + {{ 'calculated-fields.entity-zone-relationship' | translate }} +
+
+
+
calculated-fields.level
+
calculated-fields.direction-level
+
calculated-fields.relation-type
+
+
+ @if (levelsFormArray()?.controls?.length) { +
+ @for (keyControl of levelsFormArray().controls; track trackByKey; ) { +
+
+ +
+
+
{{ $index + 1 }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} + } + + + + +
+
+ +
+
+ } +
+ } @else { + {{ 'calculated-fields.no-level' | translate }} + } + @if (levelsFormArray().errors) { + + } +
+
+ @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { +
+ warning + {{ 'calculated-fields.max-allowed-levels-error' | translate }} +
+ } @else { + + } +
+
+
+
+ + @if (entityFilter.singleEntity?.id) { +
+
+ {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
+ @if (entityType === ArgumentEntityType.RelationQuery) { + + + @if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) { + + warning + + } + + } @else { + + } +
+ } +
+
{{ 'calculated-fields.report-strategy' | translate }}
+ + + @for (strategy of GeofencingReportStrategyList; track strategy) { + {{ GeofencingReportStrategyTranslations.get(strategy) | translate }} + } + + +
+
+
+ +
+ {{ 'calculated-fields.create-relation-with-matched-zones' | translate }} +
+
+
+
{{ 'calculated-fields.direction' | translate }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
+
+
{{ 'calculated-fields.relation-type' | translate }}
+ + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss similarity index 81% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss index ac5dc70ef9..3768392229 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss @@ -15,20 +15,7 @@ */ @import '../../../../../../../scss/constants'; -$panel-width: 520px; - :host { - display: flex; - width: $panel-width; - max-width: 100%; - max-height: 80vh; - - .fixed-title-width { - @media #{$mat-xs} { - min-width: 120px; - } - } - .level-text { display: flex; justify-content: center; @@ -58,7 +45,7 @@ $panel-width: 520px; } .limit-field-row { - @media screen and (max-width: $panel-width) { + @media screen and (max-width: 520px) { display: flex; flex-direction: column; @@ -71,11 +58,6 @@ $panel-width: 520px; } :host ::ng-deep { - .time-interval-field { - .advanced-input { - flex-direction: column; - } - } tb-entity-autocomplete { .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { padding-right: 0 !important; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts similarity index 91% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 8e5921516e..0e18521572 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -32,14 +32,13 @@ import { ArgumentEntityTypeTranslations, CalculatedFieldGeofencing, CalculatedFieldGeofencingValue, - CalculatedFieldType, GeofencingDirectionLevelTranslations, GeofencingDirectionTranslations, GeofencingReportStrategy, GeofencingReportStrategyTranslations, getCalculatedFieldCurrentEntityFilter } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, map } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; @@ -58,7 +57,7 @@ import { CdkDragDrop } from "@angular/cdk/drag-drop"; @Component({ selector: 'tb-calculated-field-geofencing-zone-groups-panel', templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', - styleUrls: ['./calculated-field-geofencing-zone-groups-panel.component.scss'] + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss'] }) export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit { @@ -67,7 +66,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() calculatedFieldType: CalculatedFieldType; + @Input() ownerId: EntityId; @Input() usedNames: string[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -118,7 +117,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit this.observeEntityFilterChanges(); this.observeEntityTypeChanges(); - this.observeUpdatePosition(); this.observeCreateRelationZonesChanges(); } @@ -161,26 +159,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); } - private observeMaxLevelChanges(): void { - this.refDynamicSourceFormGroup.get('maxLevel').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(value => this.validateFetchLastLevelOnly(value)); - } - private observeCreateRelationZonesChanges(): void { this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges .pipe(takeUntilDestroyed()) .subscribe(value => this.validateDirectionAndRelationType(value)); } - private validateFetchLastLevelOnly(maxLevel = 1): void { - if (maxLevel > 1) { - this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').enable({emitEvent: false}); - } else { - this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').disable({emitEvent: false}); - } - } - private validateDirectionAndRelationType(createRelation = false): void { if (createRelation) { this.geofencingFormGroup.get('direction').enable({emitEvent: false}); @@ -212,6 +196,11 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit case ArgumentEntityType.Current: delete value.refEntityId; break; + case ArgumentEntityType.Owner: + delete value.refEntityId; + value.refDynamicSourceConfiguration ||= { type: ArgumentEntityType.Owner }; + value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner; + break; case ArgumentEntityType.RelationQuery: delete value.refEntityId; value.refDynamicSourceConfiguration.type = ArgumentEntityType.RelationQuery; @@ -236,6 +225,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit case ArgumentEntityType.RelationQuery: entityFilter = this.currentEntityFilter; break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; case ArgumentEntityType.Tenant: entityFilter = { type: AliasFilterType.singleEntity, @@ -304,21 +299,11 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit }; } - private observeUpdatePosition(): void { - merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges, - this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges - ) - .pipe(delay(50), takeUntilDestroyed()) - .subscribe(() => this.popover.updatePosition()); - } - levelsFormArray(): UntypedFormArray { return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray; } - trackByKey(index: number, keyControl: AbstractControl): any { + trackByKey(_index: number, keyControl: AbstractControl): any { return keyControl; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html similarity index 96% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index f8bd9371f0..dcfd37796d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -35,13 +35,15 @@ - + {{ 'entity.entity-type' | translate }}
@if (geofenceZone.refEntityId?.entityType === ArgumentEntityType.Tenant) { {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery) { {{ 'calculated-fields.argument-relation-query' | translate }} } @else if (geofenceZone.refEntityId?.id) { @@ -91,7 +93,7 @@ -
+
-
-
-
{{ $index+1 }}
- - - @for (direction of GeofencingDirectionList; track direction) { - {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} - } - - - - -
-
- -
-
- } -
- } @else { - {{ 'calculated-fields.no-level' | translate }} - } - @if (levelsFormArray().errors) { - - } -
-
- @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { -
- warning - {{ 'calculated-fields.max-allowed-levels-error' | translate }} -
- } @else { - - } -
- -
-
- - @if (entityFilter.singleEntity.id) { -
-
- {{ 'calculated-fields.perimeter-attribute-key' | translate }} -
- @if (entityType === ArgumentEntityType.RelationQuery) { - - - @if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) { - - warning - - } @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) { - - warning - - } - - } @else { - - } -
- } -
-
{{ 'calculated-fields.report-strategy' | translate }}
- - - @for (strategy of GeofencingReportStrategyList; track strategy) { - {{ GeofencingReportStrategyTranslations.get(strategy) | translate }} - } - - -
-
-
- -
- {{ 'calculated-fields.create-relation-with-matched-zones' | translate }} -
-
-
-
{{ 'calculated-fields.direction' | translate }}
- - - @for (direction of GeofencingDirectionList; track direction) { - {{ GeofencingDirectionTranslations.get(direction) | translate }} - } - - -
-
-
{{ 'calculated-fields.relation-type' | translate }}
- - -
-
-
-
-
- - -
-
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html new file mode 100644 index 0000000000..b6f0343885 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -0,0 +1,99 @@ + +
+
+
+ {{ 'calculated-fields.propagation-path-related-entities' | translate }} +
+
+ + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
+
+
+
+
+ {{ 'calculated-fields.data-propagate' | translate }} +
+ + {{ 'calculated-fields.propagate-type.arguments-only' | translate }} + {{ 'calculated-fields.propagate-type.expression-result' | translate }} + +
+ +
+
+
+ {{ 'calculated-fields.expression' | translate }} +
+
+ +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts new file mode 100644 index 0000000000..4fb8bb561e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -0,0 +1,175 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + calculatedFieldDefaultScript, + CalculatedFieldOutput, + CalculatedFieldPropagationConfiguration, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations, + PropagationWithExpression +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; + +@Component({ + selector: 'tb-propagation-configuration', + templateUrl: './propagation-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + } + ], +}) +export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + testScript: () => Observable; + + propagateConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + applyExpressionToResolvedArguments: [false], + relation: this.fb.group({ + direction: [EntitySearchDirection.TO, Validators.required], + relationType: ['Contains', Validators.required], + }), + expression: [calculatedFieldDefaultScript], + output: this.fb.control({ + type: OutputType.Timeseries, + }), + }); + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + + functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.updatedFormWithScript(); + }) + + this.propagateConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldPropagationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.propagateConfiguration.valid || this.propagateConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: PropagationWithExpression): void { + value.expression = value.expression ?? calculatedFieldDefaultScript; + this.propagateConfiguration.patchValue(value, {emitEvent: false}); + this.updatedFormWithScript(); + setTimeout(() => { + this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.propagateConfiguration.disable({emitEvent: false}); + } else { + this.propagateConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.propagateConfiguration.get('expression').setValue(expression); + this.propagateConfiguration.get('expression').markAsDirty(); + }) + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldPropagationConfiguration): void { + value.type = CalculatedFieldType.PROPAGATION; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) { + this.propagateConfiguration.get('expression').enable({emitEvent: false}); + } else { + this.propagateConfiguration.get('expression').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts new file mode 100644 index 0000000000..83dc4badf9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + PropagationConfigurationComponent +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + PropagationConfigurationComponent, + ], + exports: [ + PropagationConfigurationComponent, + ] +}) +export class PropagationConfigurationModule { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index 9e3c52bc4f..d4d4f9d1da 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -15,7 +15,5 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './arguments-table/calculated-field-arguments-table.component'; -export * from './panel/calculated-field-argument-panel.component'; export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html new file mode 100644 index 0000000000..8b29689b7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html @@ -0,0 +1,175 @@ + +
+
{{ 'calculated-fields.metrics.metric-settings' | translate }}
+
+
+
{{ 'calculated-fields.metrics.metric-name' | translate }}
+ + + @if (metricForm.get('name').touched && metricForm.get('name').hasError('required')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('duplicateName')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('pattern')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('maxlength')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('forbiddenName')) { + + warning + + } + +
+
+
{{ 'calculated-fields.metrics.aggregation' | translate }}
+ + + @for (aggFunction of AggFunctions; track aggFunction) { + {{ AggFunctionTranslations.get(aggFunction) | translate }} + } + + +
+ +
+ + + + +
+ {{ 'calculated-fields.metrics.filter' | translate }} +
+
+
+
+ + +
{{ 'api-usage.tbel' | translate }} +
+
+
+
+
+ +
+
{{ 'calculated-fields.metrics.value-source' | translate }}
+ + + @for (inputType of AggInputTypes; track inputType) { + {{ AggInputTypeTranslations.get(inputType) | translate }} + } + + +
+ @if (this.metricForm.get('input.type').value === AggInputType.key) { +
+
{{ 'calculated-fields.argument-name' | translate }}
+ + + @for (argument of arguments; track argument) { + {{ argument }} + } + + @if (metricForm.get('input.key').touched && metricForm.get('input.key').hasError('required')) { + + warning + + } + +
+ } @else { + +
{{ 'api-usage.tbel' | translate }} +
+
+ } +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts new file mode 100644 index 0000000000..d21db8ece9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts @@ -0,0 +1,167 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, output } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormBuilder, FormControl, ValidatorFn, Validators } from '@angular/forms'; +import { charsWithNumRegex } from '@shared/models/regex.constants'; +import { + AggFunction, + AggFunctionTranslations, + AggInputType, + AggInputTypeTranslations, + CalculatedFieldAggMetricValue +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { + allowFilter: boolean; +} + +@Component({ + selector: 'tb-calculated-field-metrics-panel', + templateUrl: './calculated-field-metrics-panel.component.html', + styleUrl: '../common/calculated-field-panel.scss', +}) +export class CalculatedFieldMetricsPanelComponent implements OnInit { + + @Input() buttonTitle: string; + @Input() metric: CalculatedFieldAggMetricValue; + @Input() usedNames: string[]; + @Input() arguments: Array; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + + metricDataApplied = output(); + filterExpanded = false; + functionArgs: Array + + metricForm = this.fb.group({ + name: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + function: [AggFunction.AVG], + allowFilter: [false], + filter: ['', Validators.required], + input: this.fb.group({ + type: [AggInputType.key], + key: ['', Validators.required], + function: ['', Validators.required], + }) + }); + + entityFilter: EntityFilter; + + readonly AggFunctions = Object.values(AggFunction) as AggFunction[]; + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly ScriptLanguage = ScriptLanguage; + readonly AggInputType = AggInputType; + readonly AggInputTypes = Object.values(AggInputType) as AggInputType[]; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + + constructor( + private fb: FormBuilder, + private popover: TbPopoverComponent + ) { + this.observeFilterAllowChange(); + this.observeInputTypeChange(); + } + + ngOnInit(): void { + const data: CalculatedFieldAggMetricValuePanel = { + ...this.metric, + allowFilter: !!this.metric.filter, + } + this.metricForm.patchValue(data, {emitEvent: false}); + + this.validateFilter(data.allowFilter); + this.validateInputTypeFilter(data.input?.type ?? AggInputType.key); + this.validateInputKey(); + + this.functionArgs = ['ctx', ...this.arguments]; + } + + saveMetric(): void { + const value = this.metricForm.value as CalculatedFieldAggMetricValuePanel; + if (!value.allowFilter) { + delete value.filter; + } + delete value.allowFilter; + this.metricDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private observeFilterAllowChange(): void { + this.metricForm.get('allowFilter').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateFilter(value)); + } + + private observeInputTypeChange(): void { + this.metricForm.get('input.type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateInputTypeFilter(value)); + } + + private validateFilter(allowFilter = false): void { + if (allowFilter) { + this.metricForm.get('filter').enable({emitEvent: false}); + } else { + this.metricForm.get('filter').disable({emitEvent: false}); + } + this.filterExpanded = allowFilter; + } + + private validateInputTypeFilter(value: AggInputType): void { + const inputForm = this.metricForm.get('input'); + if (value === AggInputType.key) { + inputForm.get('key').enable({emitEvent: false}); + inputForm.get('function').disable({emitEvent: false}); + } else { + inputForm.get('key').disable({emitEvent: false}); + inputForm.get('function').enable({emitEvent: false}); + } + } + + private validateInputKey() { + if (this.metric.input?.type === AggInputType.key && !this.arguments.includes(this.metric.input.key)) { + this.metricForm.get('input.key').setValue(null); + this.metricForm.get('input.key').markAsTouched(); + } + } + + private uniqNameRequired(): ValidatorFn { + return (control: FormControl) => { + const newName = control.value.trim().toLowerCase(); + const isDuplicate = this.usedNames?.some(name => name.toLowerCase() === newName); + + return isDuplicate ? { duplicateName: true } : null; + }; + } + + private forbiddenNameValidator(): ValidatorFn { + return (control: FormControl) => { + const trimmedValue = control.value.trim().toLowerCase(); + const forbiddenNames = ['ctx', 'e', 'pi']; + return forbiddenNames.includes(trimmedValue) ? { forbiddenName: true } : null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html new file mode 100644 index 0000000000..4e05a0bbd5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html @@ -0,0 +1,113 @@ + +
+
+ + + +
{{ 'calculated-fields.metrics.metric-name' | translate }}
+
+ +
+
{{ metric.name }}
+ +
+
+
+ + + {{ 'calculated-fields.metrics.aggregation' | translate }} + + +
{{ AggFunctionTranslations.get(metric.function) | translate }}
+
+
+ + + {{ 'calculated-fields.metrics.filtered' | translate }} + + +
+ {{ metric.filter ? 'check_box' : 'check_box_outline_blank' }} +
+
+
+ + + {{ 'calculated-fields.metrics.value-source' | translate }} + + +
{{ AggInputTypeTranslations.get(metric.input.type) | translate }}
+
+
+ + + + +
+ + +
+
+
+ + +
+
+ {{ 'calculated-fields.metrics.no-metrics-configured' | translate }} +
+ @if (errorText) { + + } +
+
+ + @if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { +
+ warning + {{ 'calculated-fields.metrics.max-metrics' | translate }} +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts new file mode 100644 index 0000000000..3689e016b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts @@ -0,0 +1,244 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + AggFunctionTranslations, + AggInputTypeTranslations, + CalculatedFieldAggMetric, + CalculatedFieldAggMetricValue, +} from '@shared/models/calculated-field.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-calculated-field-metrics-table', + templateUrl: './calculated-field-metrics-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + } + ], +}) +export class CalculatedFieldMetricsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { + + @Input() arguments: Array; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + metricsFormArray = this.fb.array([]); + sortOrder = { direction: 'asc' as SortDirection, property: '' }; + dataSource = new CalculatedFieldMetricsDatasource(); + + displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions'] + + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private destroyRef: DestroyRef, + private store: Store + ) { + this.metricsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getMetricsObject(value)); + }); + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.metricsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { metricsFormArray: false } : null; + } + + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { + $event.stopPropagation(); + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + this.metricsFormArray.removeAt(index); + this.metricsFormArray.markAsDirty(); + } + + manageMetrics($event: Event, matButton: MatButton, metric = {} as CalculatedFieldAggMetricValue): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + const isExists = index !== -1; + const ctx = { + index, + metric, + buttonTitle: isExists ? 'action.apply' : 'action.add', + usedNames: this.metricsFormArray.value.map(({ name }) => name).filter(name => name !== metric.name), + arguments: this.arguments, + editorCompleter: this.editorCompleter, + highlightRules: this.highlightRules + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldMetricsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.metricDataApplied.subscribe((value) => { + this.popoverComponent.hide(); + if (isExists) { + this.metricsFormArray.at(index).setValue(value); + } else { + this.metricsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldAggMetricValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (!this.metricsFormArray.controls.length) { + this.errorText = 'calculated-fields.metrics.metrics-empty'; + } else { + this.errorText = ''; + } + } + + private getMetricsObject(value: CalculatedFieldAggMetricValue[]): Record { + return value.reduce((acc, metricValue) => { + const { name, ...metric } = metricValue; + acc[name] = metric; + return acc; + }, {} as Record); + } + + writeValue(metrics: Record): void { + this.metricsFormArray.clear(); + this.populateZonesFormArray(metrics); + } + + private populateZonesFormArray(metrics: Record): void { + Object.keys(metrics).forEach(key => { + const value: CalculatedFieldAggMetricValue = { + ...metrics[key], + name: key + }; + this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.metricsFormArray.updateValueAndValidity(); + } + + private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { + switch (column) { + case 'function': + return metric.function; + case 'valueSource': + return metric.input?.type; + case 'filter': + return isDefinedAndNotNull(metric.filter).toString(); + default: + return metric.name; + } + } + + private sortData(data: CalculatedFieldAggMetricValue[]): CalculatedFieldAggMetricValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldMetricsDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html new file mode 100644 index 0000000000..914226b777 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -0,0 +1,81 @@ + +
+
+
+ {{ 'calculated-fields.aggregation-path-related-entities' | translate }} +
+
+ + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
+
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.metrics.metrics' | translate }} +
+ + + +
+ +
+ +
+
calculated-fields.use-latest-timestamp
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss new file mode 100644 index 0000000000..6a8faea40f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .simpleMode { + min-width: 0; + + .mat-slide { + overflow: hidden; + + .mdc-form-field { + width: 100%; + + .mdc-label { + min-width: 0; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts new file mode 100644 index 0000000000..c53f400916 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + CalculatedFieldOutput, + CalculatedFieldRelatedAggregationConfiguration, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + styleUrl: './related-entities-aggregation-component.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], +}) +export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; + + relatedAggregationConfiguration = this.fb.group({ + relation: this.fb.group({ + direction: [EntitySearchDirection.FROM, Validators.required], + relationType: ['Contains', Validators.required], + }), + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], + output: this.fb.control({ + type: OutputType.Timeseries, + }), + useLatestTs: [false] + }); + + arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + private propagateChange: (config: CalculatedFieldRelatedAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.relatedAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { + this.relatedAggregationConfiguration.patchValue(value, {emitEvent: false}); + setTimeout(() => { + this.relatedAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldRelatedAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.relatedAggregationConfiguration.disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.enable({emitEvent: false}); + } + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldRelatedAggregationConfiguration): void { + value.type = CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + value.scheduledUpdateInterval = this.minAllowedScheduledUpdateIntervalInSecForCF; + this.propagateChange(value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts new file mode 100644 index 0000000000..b272f1e965 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + RelatedEntitiesAggregationComponentComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component'; +import { + CalculatedFieldMetricsTableComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + RelatedEntitiesAggregationComponentComponent, + CalculatedFieldMetricsTableComponent, + CalculatedFieldMetricsPanelComponent + ], + exports: [ + RelatedEntitiesAggregationComponentComponent, + ] +}) +export class RelatedEntitiesAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html new file mode 100644 index 0000000000..cfab9d9def --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -0,0 +1,99 @@ + +
+
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
+ {{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} +
+ + +
+
+ @if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) { + + @if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } @else { + {{ 'calculated-fields.hint.expression' | translate }} + } +
+
+ +
{{ 'api-usage.tbel' | translate }} +
+ +
+
+ +
+
+
+ +
+ +
+ calculated-fields.use-latest-timestamp +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts new file mode 100644 index 0000000000..065556081c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + calculatedFieldDefaultScript, + CalculatedFieldScriptConfiguration, + CalculatedFieldSimpleConfiguration, + CalculatedFieldSimpleOutput, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + OutputType +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { deepClone } from '@core/utils'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable } from 'rxjs'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { map } from 'rxjs/operators'; + +type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; + +@Component({ + selector: 'tb-simple-configuration', + templateUrl: './simple-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + } + ], +}) +export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input() + isScript: boolean; + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({required: true}) + testScript: () => Observable; + + simpleConfiguration = this.fb.group({ + arguments: this.fb.control({}), + expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + expressionSCRIPT: [calculatedFieldDefaultScript], + output: this.fb.control({ + name: '', + type: OutputType.Timeseries, + decimalsByDefault: null + }), + useLatestTs: [false] + }); + + readonly ScriptLanguage = ScriptLanguage; + readonly OutputType = OutputType; + + functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: SimpeConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + this.simpleConfiguration.get('output').valueChanges.pipe( + takeUntilDestroyed(), + ).subscribe(() => { + this.toggleScopeByOutputType(); + }); + + this.simpleConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + const { expressionSIMPLE, expressionSCRIPT, ...config } = value; + const cfConfig = config as SimpeConfiguration; + cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE; + this.updatedModel(cfConfig); + }) + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'isScript') { + this.updatedFormWithScript(); + if (!change.firstChange) { + this.simpleConfiguration.updateValueAndValidity(); + } + } + } + } + } + + validate(): ValidationErrors | null { + return this.simpleConfiguration.valid || this.simpleConfiguration.disabled ? null : {invalidSimpleConfig: false}; + } + + writeValue(value: SimpeConfiguration): void { + const formValue: any = deepClone(value); + if (this.isScript) { + formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; + } else { + formValue.expressionSIMPLE = formValue.expression; + } + this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); + this.updatedFormWithScript(); + setTimeout(() => { + this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: SimpeConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.simpleConfiguration.disable({emitEvent: false}); + } else { + this.simpleConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); + this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); + }) + } + + private updatedModel(value: SimpeConfiguration): void { + value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.isScript) { + this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false}); + } else { + this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false}); + } + this.toggleScopeByOutputType(); + } + + private toggleScopeByOutputType(): void { + if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) { + this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); + } else { + this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts new file mode 100644 index 0000000000..2e5e14426e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + SimpleConfigurationComponent +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + SimpleConfigurationComponent, + ], + exports: [ + SimpleConfigurationComponent + ] +}) +export class SimpleConfigurationModule {} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 0099fa83a7..44d1e2efc3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -649,9 +649,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private hideToolbarSetting(): boolean { - if (this.dashboard.configuration.settings && - isDefined(this.dashboard.configuration.settings.hideToolbar)) { - return this.dashboard.configuration.settings.hideToolbar; + if (isDefined(this.dashboard.configuration?.settings?.hideToolbar)) { + const canApplyHideSetting = !this.forceFullscreen || this.isMobileApp; + return this.dashboard.configuration.settings.hideToolbar && canApplyHideSetting; } else { return false; } diff --git a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts index ff052313ff..2db5dd3c97 100644 --- a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts @@ -24,6 +24,7 @@ import { EntityComponent } from './entity.component'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { CountryData } from '@shared/models/country.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { validateEmail } from '@app/core/utils'; @Directive() export abstract class ContactBasedComponent> extends EntityComponent implements AfterViewInit { @@ -50,7 +51,7 @@ export abstract class ContactBasedComponent> exten entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [Validators.maxLength(255)])); - entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); + entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [validateEmail])); return entityForm; } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index e19407a687..c3083d2180 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -53,6 +53,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements @Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false; + @Input() entityLabel = ''; @Input() entityType: EntityType; @Input() allEnabledUntil = 0; @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @@ -64,7 +65,6 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements maxMessagesCount: string; maxTimeFrameDuration: number; initialAllEnabled: boolean; - entityLabel: string; isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( startWith(this.debugAllControl.value), @@ -109,7 +109,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements this.onFailuresControl.patchValue(this.failuresEnabled); this.debugAllControl.patchValue(this.allEnabled); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); - this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + if (!this.entityLabel) { + this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + } } onCancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts index 0af9da52f2..f3b19d3737 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts @@ -28,6 +28,7 @@ export interface EntityDebugSettingPanelConfig { maxDebugModeDuration?: number; additionalActionConfig?: AdditionalDebugActionConfig; entityType: EntityType; + entityLabel?: string; } onSettingsAppliedFn: (settings: EntityDebugSettings) => void; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 25bb1d9700..736e716bec 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -45,6 +45,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index 4c350beee7..6ec45f2d16 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ filter.filter }}

+

{{ filter.filter | customTranslate }}

+
+ + tenant-profile.max-related-level-per-argument + + + {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} + + + {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} + + + + + tenant-profile.min-allowed-scheduled-update-interval + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | translate}} + + + +
+
+ + tenant-profile.relation-search-entity-limit + + + {{ 'tenant-profile.relation-search-entity-limit-required' | translate}} + + + {{ 'tenant-profile.relation-search-entity-limit-range' | translate}} + + tenant-profile.relation-search-entity-limit-hint + + + tenant-profile.min-allowed-deduplication-interval + + + {{ 'tenant-profile.min-allowed-deduplication-interval-required' | translate}} + + + {{ 'tenant-profile.min-allowed-deduplication-interval-range' | translate}} + + + +
@@ -354,10 +382,10 @@ - + {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} - + {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} @@ -367,10 +395,10 @@ - + {{ 'tenant-profile.alarms-ttl-days-required' | translate}} - + {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} @@ -382,10 +410,10 @@ - + {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} - + {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} @@ -395,10 +423,10 @@ - + {{ 'tenant-profile.rpc-ttl-days-required' | translate}} - + {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} @@ -410,10 +438,10 @@ - + {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} - + {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} @@ -423,10 +451,10 @@ - + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} - + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} @@ -441,16 +469,16 @@ {{ 'tenant-profile.sms-enabled' | translate }} - tenant-profile.max-sms - + {{ 'tenant-profile.max-sms-required' | translate}} - + {{ 'tenant-profile.max-sms-range' | translate}} @@ -460,10 +488,10 @@ - + {{ 'tenant-profile.max-emails-required' | translate}} - + {{ 'tenant-profile.max-emails-range' | translate}} @@ -473,10 +501,10 @@ - + {{ 'tenant-profile.max-created-alarms-required' | translate}} - + {{ 'tenant-profile.max-created-alarms-range' | translate}} @@ -494,7 +522,7 @@ - + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} @@ -513,10 +541,10 @@ - + {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} - + {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} @@ -526,10 +554,10 @@ - + {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} - + {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} @@ -541,10 +569,10 @@ - + {{ 'tenant-profile.maximum-resource-size-required' | translate}} - + {{ 'tenant-profile.maximum-resource-size-range' | translate}} @@ -561,14 +589,14 @@ tenant-profile.ws-limit-max-sessions-per-tenant - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-tenant - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -577,14 +605,14 @@ tenant-profile.ws-limit-max-sessions-per-customer - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-customer - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -600,14 +628,14 @@ tenant-profile.ws-limit-max-sessions-per-public-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-public-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -616,14 +644,14 @@ tenant-profile.ws-limit-max-sessions-per-regular-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-regular-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -632,7 +660,7 @@ tenant-profile.ws-limit-queue-per-session - + {{ 'tenant-profile.too-small-value-one' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index 0dd1453648..0000d01995 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -14,16 +14,13 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model'; +import { Component, forwardRef, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { DefaultTenantProfileConfiguration, FormControlsFrom } from '@shared/models/tenant.model'; import { isDefinedAndNotNull } from '@core/utils'; import { RateLimitsType } from './rate-limits/rate-limits.models'; -import { takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-default-tenant-profile-configuration', @@ -35,112 +32,108 @@ import { Subject } from 'rxjs'; multi: true }] }) -export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { +export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor { - defaultTenantProfileConfigurationFormGroup: UntypedFormGroup; + tenantProfileConfigurationForm: FormGroup>; - private requiredValue: boolean; - private destroy$ = new Subject(); - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() + @coerceBoolean() disabled: boolean; rateLimitsType = RateLimitsType; - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - private fb: UntypedFormBuilder) { - this.defaultTenantProfileConfigurationFormGroup = this.fb.group({ - maxDevices: [null, [Validators.required, Validators.min(0)]], - maxAssets: [null, [Validators.required, Validators.min(0)]], - maxCustomers: [null, [Validators.required, Validators.min(0)]], - maxUsers: [null, [Validators.required, Validators.min(0)]], - maxDashboards: [null, [Validators.required, Validators.min(0)]], - maxRuleChains: [null, [Validators.required, Validators.min(0)]], - maxEdges: [null, [Validators.required, Validators.min(0)]], - maxResourcesInBytes: [null, [Validators.required, Validators.min(0)]], - maxOtaPackagesInBytes: [null, [Validators.required, Validators.min(0)]], - maxResourceSize: [null, [Validators.required, Validators.min(0)]], - transportTenantMsgRateLimit: [null, []], - transportTenantTelemetryMsgRateLimit: [null, []], - transportTenantTelemetryDataPointsRateLimit: [null, []], - transportDeviceMsgRateLimit: [null, []], - transportDeviceTelemetryMsgRateLimit: [null, []], - transportDeviceTelemetryDataPointsRateLimit: [null, []], - transportGatewayMsgRateLimit: [null, []], - transportGatewayTelemetryMsgRateLimit: [null, []], - transportGatewayTelemetryDataPointsRateLimit: [null, []], - transportGatewayDeviceMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryDataPointsRateLimit: [null, []], - tenantEntityExportRateLimit: [null, []], - tenantEntityImportRateLimit: [null, []], - tenantNotificationRequestsRateLimit: [null, []], - tenantNotificationRequestsPerRuleRateLimit: [null, []], - maxTransportMessages: [null, [Validators.required, Validators.min(0)]], - maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], - maxREExecutions: [null, [Validators.required, Validators.min(0)]], - maxJSExecutions: [null, [Validators.required, Validators.min(0)]], - maxTbelExecutions: [null, [Validators.required, Validators.min(0)]], - maxDPStorageDays: [null, [Validators.required, Validators.min(0)]], - maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]], - maxEmails: [null, [Validators.required, Validators.min(0)]], - maxSms: [null, []], - smsEnabled: [null, []], - maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], - maxDebugModeDurationMinutes: [null, [Validators.min(0)]], - defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]], - alarmsTtlDays: [null, [Validators.required, Validators.min(0)]], - rpcTtlDays: [null, [Validators.required, Validators.min(0)]], - queueStatsTtlDays: [null, [Validators.required, Validators.min(0)]], - ruleEngineExceptionsTtlDays: [null, [Validators.required, Validators.min(0)]], - tenantServerRestLimitsConfiguration: [null, []], - customerServerRestLimitsConfiguration: [null, []], - maxWsSessionsPerTenant: [null, [Validators.min(0)]], - maxWsSessionsPerCustomer: [null, [Validators.min(0)]], - maxWsSessionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSessionsPerPublicUser: [null, [Validators.min(0)]], - wsMsgQueueLimitPerSession: [null, [Validators.min(0)]], - maxWsSubscriptionsPerTenant: [null, [Validators.min(0)]], - maxWsSubscriptionsPerCustomer: [null, [Validators.min(0)]], - maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]], - wsUpdatesPerSessionRateLimit: [null, []], - cassandraWriteQueryTenantCoreRateLimits: [null, []], - cassandraReadQueryTenantCoreRateLimits: [null, []], - cassandraWriteQueryTenantRuleEngineRateLimits: [null, []], - cassandraReadQueryTenantRuleEngineRateLimits: [null, []], - edgeEventRateLimits: [null, []], - edgeEventRateLimitsPerEdge: [null, []], - edgeUplinkMessagesRateLimits: [null, []], - edgeUplinkMessagesRateLimitsPerEdge: [null, []], - maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], - maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], - maxRelationLevelPerCfArgument: [null, [Validators.required, Validators.min(1)]], - minAllowedScheduledUpdateIntervalInSecForCF: [null, [Validators.required, Validators.min(0)]], - maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], - maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], - calculatedFieldDebugEventsRateLimit: [null, []], - maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]], + private propagateChange = (_v: any) => { }; + + constructor(private fb: FormBuilder) { + this.tenantProfileConfigurationForm = this.fb.group({ + maxDevices: [0, [Validators.required, Validators.min(0)]], + maxAssets: [0, [Validators.required, Validators.min(0)]], + maxCustomers: [0, [Validators.required, Validators.min(0)]], + maxUsers: [0, [Validators.required, Validators.min(0)]], + maxDashboards: [0, [Validators.required, Validators.min(0)]], + maxRuleChains: [0, [Validators.required, Validators.min(0)]], + maxEdges: [0, [Validators.required, Validators.min(0)]], + maxResourcesInBytes: [0, [Validators.required, Validators.min(0)]], + maxOtaPackagesInBytes: [0, [Validators.required, Validators.min(0)]], + maxResourceSize: [0, [Validators.required, Validators.min(0)]], + transportTenantMsgRateLimit: [''], + transportTenantTelemetryMsgRateLimit: [''], + transportTenantTelemetryDataPointsRateLimit: [''], + transportDeviceMsgRateLimit: [''], + transportDeviceTelemetryMsgRateLimit: [''], + transportDeviceTelemetryDataPointsRateLimit: [''], + transportGatewayMsgRateLimit: [''], + transportGatewayTelemetryMsgRateLimit: [''], + transportGatewayTelemetryDataPointsRateLimit: [''], + transportGatewayDeviceMsgRateLimit: [''], + transportGatewayDeviceTelemetryMsgRateLimit: [''], + transportGatewayDeviceTelemetryDataPointsRateLimit: [''], + tenantEntityExportRateLimit: [''], + tenantEntityImportRateLimit: [''], + tenantNotificationRequestsRateLimit: [''], + tenantNotificationRequestsPerRuleRateLimit: [''], + maxTransportMessages: [0, [Validators.required, Validators.min(0)]], + maxTransportDataPoints: [0, [Validators.required, Validators.min(0)]], + maxREExecutions: [0, [Validators.required, Validators.min(0)]], + maxJSExecutions: [0, [Validators.required, Validators.min(0)]], + maxTbelExecutions: [0, [Validators.required, Validators.min(0)]], + maxDPStorageDays: [0, [Validators.required, Validators.min(0)]], + maxRuleNodeExecutionsPerMessage: [0, [Validators.required, Validators.min(0)]], + maxEmails: [0, [Validators.required, Validators.min(0)]], + maxSms: [0], + smsEnabled: [false], + maxCreatedAlarms: [0, [Validators.required, Validators.min(0)]], + maxDebugModeDurationMinutes: [0, [Validators.min(0)]], + defaultStorageTtlDays: [0, [Validators.required, Validators.min(0)]], + alarmsTtlDays: [0, [Validators.required, Validators.min(0)]], + rpcTtlDays: [0, [Validators.required, Validators.min(0)]], + queueStatsTtlDays: [0, [Validators.required, Validators.min(0)]], + ruleEngineExceptionsTtlDays: [0, [Validators.required, Validators.min(0)]], + tenantServerRestLimitsConfiguration: [''], + customerServerRestLimitsConfiguration: [''], + maxWsSessionsPerTenant: [0, [Validators.min(0)]], + maxWsSessionsPerCustomer: [0, [Validators.min(0)]], + maxWsSessionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSessionsPerPublicUser: [0, [Validators.min(0)]], + wsMsgQueueLimitPerSession: [0, [Validators.min(0)]], + maxWsSubscriptionsPerTenant: [0, [Validators.min(0)]], + maxWsSubscriptionsPerCustomer: [0, [Validators.min(0)]], + maxWsSubscriptionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSubscriptionsPerPublicUser: [0, [Validators.min(0)]], + wsUpdatesPerSessionRateLimit: [''], + cassandraWriteQueryTenantCoreRateLimits: [''], + cassandraReadQueryTenantCoreRateLimits: [''], + cassandraWriteQueryTenantRuleEngineRateLimits: [''], + cassandraReadQueryTenantRuleEngineRateLimits: [''], + edgeEventRateLimits: [''], + edgeEventRateLimitsPerEdge: [''], + edgeUplinkMessagesRateLimits: [''], + edgeUplinkMessagesRateLimitsPerEdge: [''], + maxCalculatedFieldsPerEntity: [0, [Validators.required, Validators.min(0)]], + maxArgumentsPerCF: [0, [Validators.required, Validators.min(0)]], + maxRelationLevelPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedDeduplicationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + maxRelatedEntitiesToReturnPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedScheduledUpdateIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + maxDataPointsPerRollingArg: [0, [Validators.required, Validators.min(0)]], + maxStateSizeInKBytes: [0, [Validators.required, Validators.min(0)]], + calculatedFieldDebugEventsRateLimit: [''], + maxSingleValueArgumentSizeInKBytes: [0, [Validators.required, Validators.min(0)]], }); - this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.get('smsEnabled').valueChanges.pipe( + takeUntilDestroyed() ).subscribe((value: boolean) => { this.maxSmsValidation(value); } ); - this.defaultTenantProfileConfigurationFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.valueChanges.pipe( + takeUntilDestroyed() ).subscribe(() => { this.updateModel(); }); @@ -148,48 +141,40 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA private maxSmsValidation(smsEnabled: boolean) { if (smsEnabled) { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').addValidators([Validators.required, Validators.min(0)]); + this.tenantProfileConfigurationForm.get('maxSms').addValidators([Validators.required, Validators.min(0)]); } else { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').clearValidators(); + this.tenantProfileConfigurationForm.get('maxSms').clearValidators(); } - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').updateValueAndValidity({emitEvent: false}); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.tenantProfileConfigurationForm.get('maxSms').updateValueAndValidity({emitEvent: false}); } registerOnChange(fn: any): void { this.propagateChange = fn; } - registerOnTouched(fn: any): void { - } - - ngOnInit() { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.defaultTenantProfileConfigurationFormGroup.disable({emitEvent: false}); + this.tenantProfileConfigurationForm.disable({emitEvent: false}); } else { - this.defaultTenantProfileConfigurationFormGroup.enable({emitEvent: false}); + this.tenantProfileConfigurationForm.enable({emitEvent: false}); } } writeValue(value: DefaultTenantProfileConfiguration | null): void { if (isDefinedAndNotNull(value)) { this.maxSmsValidation(value.smsEnabled); - this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false}); + this.tenantProfileConfigurationForm.patchValue(value, {emitEvent: false}); } } private updateModel() { - let configuration: TenantProfileConfiguration = null; - if (this.defaultTenantProfileConfigurationFormGroup.valid) { - configuration = this.defaultTenantProfileConfigurationFormGroup.getRawValue(); + let configuration: DefaultTenantProfileConfiguration = null; + if (this.tenantProfileConfigurationForm.valid) { + configuration = this.tenantProfileConfigurationForm.getRawValue(); } this.propagateChange(configuration); } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index edce8b12a9..a542d3e07d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -39,6 +39,6 @@ > diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index d80892664d..2bf50da468 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -38,7 +38,7 @@
- +
@@ -60,6 +60,7 @@
-
{{ 'version-control.export-credentials' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss index a72bd2ca77..e925c79b0d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss @@ -52,7 +52,6 @@ padding: 0 8px 8px; tb-branch-autocomplete { min-width: 200px; - max-width: 200px; display: block; } } diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html index 1e1f89bfb8..e6d9ec34bc 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html @@ -41,7 +41,7 @@
- + version-control.default-sync-strategy @for (strategy of syncStrategies; track strategy) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 968ad06a14..ff1ce791cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -330,6 +330,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (this.displayPagination) { this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.paginator.pageIndex = 0); + + this.ctx.aliasController?.filtersChanged.pipe( + takeUntil(this.destroy$) + ).subscribe((filters) => { + let currentFilterId = this.ctx.defaultSubscription.options.alarmSource?.filterId; + if (currentFilterId && filters.includes(currentFilterId)) { + this.paginator.firstPage(); + } + }); } ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable).pipe( takeUntil(this.destroy$) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss index 2eb0ceb940..1c97102fe0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss @@ -51,7 +51,7 @@ flex-direction: row-reverse; } .tb-latest-chart-shape { - flex: 1; + flex: 1 1 100%; min-width: 10px; min-height: 10px; display: flex; @@ -95,8 +95,14 @@ &.legend-right, &.legend-left { gap: 24px; .tb-latest-chart-legend { + flex: 0 1 auto; + overflow-y: auto; + max-height: 100%; + flex-wrap: nowrap; + min-width: fit-content; + max-width: 100%; flex-direction: column; - justify-content: center; + justify-content: stretch; align-items: stretch; .tb-latest-chart-legend-item { flex-direction: row; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts index 85905dfd3e..1fa0437cea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -181,17 +181,13 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private onResize() { if (this.legendHorizontal) { - this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null); - this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null); } const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width; const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height; const size = Math.min(shapeWidth, shapeHeight); if (this.legendHorizontal) { - this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`); this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`); - this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1'); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html index 65d647993f..d1142d08ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html @@ -17,8 +17,16 @@ -->
- - {{ column.title }} + + {{'entity.show-all-columns' | translate}} + + @for (column of columns; track column.def) { + + {{ column.title }} + + }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts index 8c5f13de69..4ef00bd575 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts @@ -15,6 +15,8 @@ /// import { Component, Inject, InjectionToken } from '@angular/core'; +import { isDefinedAndNotNull } from '@core/utils'; +import { SelectableColumnsPipe } from '@shared/public-api'; import { DisplayColumn } from '@home/components/widget/lib/table-widget.models'; export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken('DisplayColumnsPanelData'); @@ -33,8 +35,28 @@ export class DisplayColumnsPanelComponent { columns: DisplayColumn[]; - constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) { - this.columns = this.data.columns; + constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData, + private selectableColumnsPipe: SelectableColumnsPipe ) { + this.columns = this.selectableColumnsPipe.transform(this.data.columns); + } + + get allColumnsVisible(): boolean { + return isDefinedAndNotNull(this.columns) && this.columns.every(column => column.display); + } + + get someColumnsVisible(): boolean { + const filtredColumns = this.columns.filter(item => item.display); + return filtredColumns.length !== 0 && this.columns.length !== filtredColumns.length; + } + + public toggleAllColumns(event: any): void { + const isChecked = event.checked; + + this.columns.forEach(column => { + column.display = isChecked; + }); + + this.update(); } public update() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index f95c8c26d7..87acdd686b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -265,6 +265,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (this.displayPagination) { this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe(() => this.paginator.pageIndex = 0); + + this.ctx.aliasController?.filtersChanged.pipe( + takeUntil(this.destroy$) + ).subscribe((filters) => { + let currentFilterId = this.ctx.defaultSubscription.options.datasources?.[0]?.filterId; + if (currentFilterId && filters.includes(currentFilterId)) { + this.paginator.firstPage(); + } + }); } ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable).pipe( takeUntil(this.destroy$) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts new file mode 100644 index 0000000000..1bb4b97b4c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts @@ -0,0 +1,466 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + defaultBasePolylinesDataLayerSettings, + isCutPolygon, + isJSON, + MapDataLayerType, + PolylinesDataLayerSettings, + TbMapDatasource, + TbPolylineCoordinates, + TbPolylineData, + TbPolylineRawCoordinates +} from '@shared/models/widget/maps/map.models'; +import L from 'leaflet'; +import { DataKey, FormattedData } from '@shared/models/widget.models'; +import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbMap } from '@home/components/widget/lib/maps/map'; +import { Observable } from 'rxjs'; +import { isNotEmptyStr, isString } from '@core/utils'; +import { + TbLatestDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; +import { map } from 'rxjs/operators'; + +class TbPolylineDataLayerItem extends TbLatestDataLayerItem { + + private polylineContainer: L.FeatureGroup; + private polyline: L.Polyline; + private polylineStyleInfo: ShapeStyleInfo; + private editing = false; + + constructor(data: FormattedData, + dsData: FormattedData[], + protected settings: PolylinesDataLayerSettings, + protected dataLayer: TbPolylineDataLayer) { + super(data, dsData, settings, dataLayer); + } + + public isEditing() { + return this.editing; + } + + public updateBubblingMouseEvents() { + this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode(); + } + + public remove() { + super.remove(); + if (this.polylineStyleInfo?.patternId) { + this.dataLayer.getMap().unUseShapePattern(this.polylineStyleInfo.patternId); + } + } + + protected create(data: FormattedData, dsData: FormattedData[]): L.Layer { + const polyData = this.dataLayer.extractPolylineCoordinates(data); + const polyConstructor = L.polyline; + this.polyline = polyConstructor(polyData as (TbPolylineRawCoordinates & L.LatLngTuple[]), { + noClip: true, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { + this.polylineStyleInfo = styleInfo; + if (this.polyline) { + this.polyline.setStyle(this.polylineStyleInfo.style); + } + }); + + this.polylineContainer = L.featureGroup(); + this.polyline.addTo(this.polylineContainer); + + this.updateLabel(data, dsData); + return this.polylineContainer; + } + + protected unbindLabel() { + this.polylineContainer.unbindTooltip(); + } + + protected bindLabel(content: L.Content): void { + this.polylineContainer.bindTooltip(content, {className: 'tb-polyline-label', permanent: true, direction: 'center'}) + .openTooltip(this.polylineContainer.getBounds().getCenter()); + } + + protected doUpdate(data: FormattedData, dsData: FormattedData[]): void { + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { + this.polylineStyleInfo = styleInfo; + this.updatePolylineShape(data); + this.updateTooltip(data, dsData); + this.updateLabel(data, dsData); + + if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) { + this.polyline.setStyle(this.polylineStyleInfo.style); + } + }); + } + + protected doInvalidateCoordinates(data: FormattedData, _dsData: FormattedData[]): void { + this.updatePolylineShape(data); + } + + protected addItemClass(clazz: string): void { + if ((this.polyline as any)._path) { + L.DomUtil.addClass((this.polyline as any)._path, clazz); + } + } + + protected removeItemClass(clazz: string): void { + if ((this.polyline as any)._path) { + L.DomUtil.removeClass((this.polyline as any)._path, clazz); + } + } + + protected enableDrag(): void { + this.polyline.pm.setOptions({ + snappable: this.dataLayer.isSnappable() + }); + this.polyline.pm.enableLayerDrag(); + this.polyline.on('pm:dragstart', () => { + this.editing = true; + }); + this.polyline.on('pm:drag', () => { + if (this.tooltip?.isOpen()) { + this.tooltip.setLatLng(this.polyline.getBounds().getCenter()); + } + }); + this.polyline.on('pm:dragend', () => { + this.savePolylineCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.polyline.pm.disableLayerDrag(); + this.polyline.off('pm:dragstart'); + this.polyline.off('pm:dragend'); + } + + protected onSelected(): L.TB.ToolbarButtonOptions[] { + const buttons: L.TB.ToolbarButtonOptions[] = []; + if (this.dataLayer.isEditEnabled()) { + this.enablePolylineEditMode(); + buttons.push( + { + id: 'cut', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.cut'), + iconClass: 'tb-cut', + click: (e, button) => { + const map = this.dataLayer.getMap().getMap(); + if (!map.pm.globalCutModeEnabled()) { + this.disablePolylineRotateMode(); + this.disablePolylineEditMode(); + this.enablePolylineCutMode(button); + } else { + this.disablePolylineCutMode(button); + this.enablePolylineEditMode(); + } + } + }, + { + id: 'rotate', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.rotate'), + iconClass: 'tb-rotate', + click: (e, button) => { + if (!this.polyline.pm.rotateEnabled()) { + this.disablePolylineCutMode(); + this.disablePolylineEditMode(); + this.enablePolylineRotateMode(button); + } else { + this.disablePolylineRotateMode(button); + this.enablePolylineEditMode(); + } + } + } + ); + } + return buttons; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.disablePolylineEditMode(); + this.disablePolylineRotateMode(); + } + } + + protected canDeselect(cancel = false): boolean { + const map = this.dataLayer.getMap().getMap(); + if (map.pm.globalCutModeEnabled()) { + if (cancel) { + this.disablePolylineCutMode(); + } + return false; + } else if (this.polyline.pm.rotateEnabled()) { + if (cancel) { + this.disablePolylineRotateMode(); + } + return false; + } else if (this.editing) { + return false; + } + return true; + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polyline.remove-polyline-for', {entityName: this.data.entityName}); + } + + protected removeDataItem(): Observable { + return this.dataLayer.savePolylineCoordinates(this.data, null); + } + + private enablePolylineEditMode() { + this.polyline.on('pm:markerdragstart', () => this.editing = true); + this.polyline.on('pm:markerdragend', () => setTimeout(() => { + this.editing = false; + })); + this.polyline.on('pm:edit', () => this.savePolylineCoordinates()); + this.polyline.pm.enable(); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(false); + } + + private disablePolylineEditMode() { + this.polyline.pm.disable(); + this.polyline.off('pm:markerdragstart'); + this.polyline.off('pm:markerdragend'); + this.polyline.off('pm:edit'); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(true); + } + + private enablePolylineCutMode(cutButton?: L.TB.ToolbarButton) { + this.polylineContainer.closePopup(); + this.editing = true; + this.polyline.options.bubblingMouseEvents = true; + this.polyline.setStyle({ + ...this.polylineStyleInfo.style, dashArray: '5 5', weight: 3, + color: '#3388ff', opacity: 1, fillColor: '#3388ff', fillOpacity: 0.2 + }); + this.addItemClass('tb-cut-mode'); + this.polyline.once('pm:cut', (e) => { + if (e.layer instanceof L.Polyline) { + this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][] , { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polyline.addTo(this.polylineContainer); + } else if (e.layer instanceof L.LayerGroup) { + const parts: L.LatLngExpression[][] = []; + + if (e.layer && typeof e.layer.getLayers === 'function') { + const segments: L.Polyline[] = e.layer.getLayers() as L.Polyline[]; + segments.forEach(segment => { + parts.push(segment.getLatLngs() as L.LatLngExpression[]); + }); + } else if (e.layer instanceof L.Polyline) { + parts.push(e.layer.getLatLngs() as L.LatLngExpression[]); + } + + if (parts.length > 0) { + this.polyline = L.polyline(parts, { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polyline.addTo(this.polylineContainer); + } + } + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + this.polylineContainer.removeLayer(this.polyline); + // @ts-ignore + this.polyline._pmTempLayer = false; + this.polyline.addTo(this.polylineContainer); + this.updateSelectedState(); + cutButton?.setActive(false); + this.savePolylineCoordinates(); + }); + const map = this.dataLayer.getMap().getMap(); + map.pm.setLang('en', { + tooltips: { + firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-cut-hint'), + finishLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.finish-polyline-cut-hint') + } + }, 'en'); + map.pm.enableGlobalCutMode({ + // @ts-ignore + layersToCut: [this.polyline] + }); + // @ts-ignore + L.DomUtil.addClass(map.pm.Draw.Cut._hintMarker.getTooltip()._container, 'tb-place-item-label'); + cutButton?.setActive(true); + map.once('pm:globalcutmodetoggled', (e) => { + if (!e.enabled) { + this.disablePolylineCutMode(cutButton); + this.enablePolylineEditMode(); + } + }); + } + + private disablePolylineCutMode(cutButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode(); + this.polyline.setStyle({...this.polylineStyleInfo.style, dashArray: null}); + this.removeItemClass('tb-cut-mode'); + this.polyline.off('pm:cut'); + const map = this.dataLayer.getMap().getMap(); + map.pm.disableGlobalCutMode(); + cutButton?.setActive(false); + } + + private enablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.polylineContainer.closePopup(); + this.editing = true; + this.polyline.on('pm:rotateend', () => { + this.savePolylineCoordinates(); + }); + this.polyline.pm.enableRotate(); + rotateButton?.setActive(true); + this.polyline.on('pm:rotatedisable', () => { + this.disablePolylineRotateMode(rotateButton); + this.enablePolylineEditMode(); + }); + } + + private disablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polyline.pm.disableRotate(); + this.polyline.off('pm:rotateend'); + this.polyline.off('pm:rotatedisable'); + rotateButton?.setActive(false); + } + + private savePolylineCoordinates() { + let coordinates: TbPolylineCoordinates = this.polyline.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolylineCoordinates; + } + if (this.polyline instanceof L.Rectangle && !isCutPolygon(coordinates)) { + const bounds = this.polyline.getBounds(); + const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()]; + if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) { + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } + } + this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe(); + } + + private updatePolylineShape(data: FormattedData) { + if (this.editing) { + return; + } + + const polyData = this.dataLayer.extractPolylineCoordinates(data) as TbPolylineData; + if (this.polyline instanceof L.Polyline && !(this.polyline instanceof L.Rectangle)) { + this.polyline.setLatLngs(polyData as L.LatLngExpression[] | L.LatLngExpression[][]); + } else if (this.polyline instanceof L.Rectangle && polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]); + this.polyline.setBounds(bounds); + } else { + this.polylineContainer.removeLayer(this.polyline); + if (polyData.length === 2 && isCutPolygon(polyData)) { + this.polyline = L.polyline(polyData as L.LatLngExpression[] | L.LatLngExpression[][], { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode(), + noClip: true + }); + } + + this.polyline.addTo(this.polylineContainer); + this.editModeUpdated(); + } + } +} + +export class TbPolylineDataLayer extends TbShapesDataLayer { + + constructor(protected map: TbMap, + inputSettings: PolylinesDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return 'polylines'; + } + + public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { + if (layer instanceof L.Polyline) { + let coordinates: TbPolylineCoordinates; + coordinates = layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolylineCoordinates; + } + this.savePolylineCoordinates(item.entity, coordinates).subscribe( + (converted) => { + item.entity[this.settings.polylineKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a polyline.'); + } + } + + public extractPolylineCoordinates(data: FormattedData): TbPolylineRawCoordinates { + let rawPolyData = data[this.settings.polylineKey.label]; + if (isString(rawPolyData)) { + rawPolyData = JSON.parse(rawPolyData); + } + return this.map.polylineDataToCoordinates(rawPolyData); + } + + public savePolylineCoordinates(data: FormattedData, coordinates: TbPolylineCoordinates): Observable { + const converted = coordinates ? this.map.coordinatesToPolylineData(coordinates) : null; + const polylineData = [ + { + dataKey: this.settings.polylineKey, + value: converted + } + ]; + return this.map.saveItemData(data.$datasource, polylineData, this.settings.edit?.attributeScope).pipe( + map(() => converted) + ); + } + + protected getDataKeys(): DataKey[] { + return [this.settings.polylineKey]; + } + + protected defaultBaseSettings(map: TbMap): Partial { + return defaultBasePolylinesDataLayerSettings(map.type()); + } + + protected doSetup(): Observable { + return super.doSetup(); + } + + protected isValidLayerData(layerData: FormattedData): boolean { + return layerData && ((isNotEmptyStr(layerData[this.settings.polylineKey.label]) && !isJSON(layerData[this.settings.polylineKey.label]) + || Array.isArray(layerData[this.settings.polylineKey.label]))); + } + + protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbPolylineDataLayerItem { + return new TbPolylineDataLayerItem(data, dsData, this.settings, this); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index 93502588b1..ef68f0a6b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -37,10 +37,12 @@ import { isDefinedAndNotNull, objectHashCode, parseTbFunction, safeExecuteTbFunc import { CompiledTbFunction } from '@shared/models/js-function.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; -export type ShapePatternStorage = {[id: string]: { - pattern: L.TB.Pattern; - refCount: number; -}}; +export type ShapePatternStorage = { + [id: string]: { + pattern: L.TB.Pattern; + refCount: number; + } +}; interface ShapePatternInfo { type: ShapeFillType; @@ -311,6 +313,23 @@ export abstract class TbShapesDataLayer, dsData: FormattedData[], fillPatternId: string): Observable { + return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( + map((patternWithId) => { + const stroke = this.strokeColorProcessor.processColor(data, dsData); + const style: L.PathOptions = { + color: stroke, + weight: this.settings.strokeWeight, + opacity: 1 + }; + return { + patternId: patternWithId.patternId, + style + } + }) + ); + } + public getShapeStyle(data: FormattedData, dsData: FormattedData[], fillPatternId: string): Observable { return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( map((patternWithId) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index ae2f62853e..bfb872c75a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts @@ -44,7 +44,7 @@ import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer type TripRouteData = {[time: number]: FormattedData}; -interface PointItem { +export interface PointItem { point: L.CircleMarker; tooltip?: L.Popup; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index fd03990d26..6093b8ee8e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -24,7 +24,11 @@ import { TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, + TbPolylineCoordinate, + TbPolylineCoordinates, + TbPolylineRawCoordinate, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; @@ -179,5 +183,29 @@ export class TbGeoMap extends TbMap { return circleData; } + public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates { + return (expression).map((el: TbPolylineRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + return el; + } else if (Array.isArray(el) && el.length) { + return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; + } else { + return null; + } + }).filter(el => !!el); + } + public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolylineCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate; + } else { + const convertPoint = latLngPointToBounds(point, this.southWest, this.northEast); + return [convertPoint.lat, convertPoint.lng]; + } + }); + } + return []; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts index 219f5d63ff..753528f006 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts @@ -23,7 +23,12 @@ import { ImageSourceType, loadImageWithAspect, MapZoomAction, - TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates + TbCircleData, + TbPolygonCoordinate, + TbPolygonCoordinates, + TbPolygonRawCoordinate, + TbPolygonRawCoordinates, TbPolylineCoordinate, TbPolylineCoordinates, TbPolylineRawCoordinate, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; @@ -339,4 +344,36 @@ export class TbImageMap extends TbMap { ); } + public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates{ + return expression.map((el: TbPolylineRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { + const latLng = this.pointToLatLng( + el[0] * this.width, + el[1] * this.height + ); + return [latLng.lat, latLng.lng] as TbPolylineRawCoordinate; + } else if (Array.isArray(el) && el.length) { + return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; + } + else { + return null; + } + }).filter(el => !!el); + } + + public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates{ + if (coordinates.length) { + return coordinates.map((point: TbPolylineCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate; + } else { + const pos = this.latLngToPoint(point); + return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)]; + } + }); + } else { + return []; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 985bdc17f5..5d00f9add6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -169,6 +169,9 @@ div.tb-widget .tb-widget-content.tb-no-interaction { &.tb-draw-circle { mask-image: url('data:image/svg+xml,'); } + &.tb-draw-polyline { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index aa8dd67494..2c111110c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -27,7 +27,9 @@ import { TbCircleData, TbMapDatasource, TbPolygonCoordinates, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, + TbPolylineCoordinates, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -80,11 +82,12 @@ import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/tr import { CompiledTbFunction } from '@shared/models/js-function.models'; import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { EntityType } from '@shared/models/entity-type.models'; +import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; +import { TbPolylineDataLayer } from '@home/components/widget/lib/maps/data-layer/polylines-data-layer'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; -import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer'; -type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; +type TooltipInstancesData = { root: HTMLElement, instances: ITooltipsterInstance[] }; export abstract class TbMap { @@ -124,6 +127,7 @@ export abstract class TbMap { protected addRectangleButton: L.TB.ToolbarButton; protected addPolygonButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton; + protected addPolylineButton: L.TB.ToolbarButton; protected timeLineComponentRef: ComponentRef; protected timeLineComponent: MapTimelinePanelComponent; @@ -132,6 +136,7 @@ export abstract class TbMap { protected addMarkerDataLayers: TbLatestMapDataLayer[]; protected addPolygonDataLayers: TbLatestMapDataLayer[]; protected addCircleDataLayers: TbLatestMapDataLayer[]; + protected addPolylineDataLayers: TbLatestMapDataLayer[]; protected shapePatternStorage: ShapePatternStorage = {}; @@ -274,6 +279,11 @@ export abstract class TbMap { this.dataLayers.push(...circlesDataLayers); this.latestDataLayers.push(...circlesDataLayers); } + if (this.settings.polylines) { + const polylinesDataLayers = this.settings.polylines.map(settings => new TbPolylineDataLayer(this, settings)); + this.dataLayers.push(...polylinesDataLayers); + this.latestDataLayers.push(...polylinesDataLayers); + } if (this.settings.trips) { const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings)); this.dataLayers.push(...tripsDataLayers); @@ -319,7 +329,7 @@ export abstract class TbMap { let datasources: TbMapDatasource[]; for (const layerType of mapDataLayerTypes) { const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType) - .map(dl => dl.getDataSources()).flat(); + .map(dl => dl.getDataSources()).flat(); if (!datasources) { datasources = typeDatasources; } else { @@ -419,88 +429,100 @@ export abstract class TbMap { private setupEditMode() { - this.editToolbar = L.TB.bottomToolbar({ - mapElement: $(this.mapElement), - closeTitle: this.ctx.translate.instant('action.cancel'), - onClose: () => { - return this.deselectItem(true); - } - }); - - this.map.on('click', () => { - this.deselectItem(); - }); - - if (this.latestDataLayers.some(dl => dl.isEditable())) { - this.map.pm.setGlobalOptions({ snappable: false }); - this.map.pm.applyGlobalOptions(); - } - - const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); - const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; - const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); - - if (showDragModeButton || addSupportedDataLayers.length) { - const drawToolbar = L.TB.toolbar({ - position: this.settings.controlsPosition - }).addTo(this.map); - if (showDragModeButton) { - this.dragModeButton = drawToolbar.toolbarButton({ - id: 'dragMode', - title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), - iconClass: 'tb-drag-mode', - click: (e, button) => { - this.toggleDragMode(e, button); - } - }); - } - this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); - if (this.addMarkerDataLayers.length) { - this.addMarkerButton = drawToolbar.toolbarButton({ - id: 'addMarker', - title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), - iconClass: 'tb-place-marker', - click: (e, button) => { - this.placeMarker(e, button); - } - }); - this.addMarkerButton.setDisabled(true); - this.setPlaceMarkerStyle(); - } - this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons'); - if (this.addPolygonDataLayers.length) { - this.addRectangleButton = drawToolbar.toolbarButton({ - id: 'addRectangle', - title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), - iconClass: 'tb-draw-rectangle', - click: (e, button) => { - this.drawRectangle(e, button); - } - }); - this.addRectangleButton.setDisabled(true); - this.addPolygonButton = drawToolbar.toolbarButton({ - id: 'addPolygon', - title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), - iconClass: 'tb-draw-polygon', - click: (e, button) => { - this.drawPolygon(e, button); - } - }); - this.addPolygonButton.setDisabled(true); - } - this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles'); - if (this.addCircleDataLayers.length) { - this.addCircleButton = drawToolbar.toolbarButton({ - id: 'addCircle', - title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), - iconClass: 'tb-draw-circle', - click: (e, button) => { - this.drawCircle(e, button); - } - }); - this.addCircleButton.setDisabled(true); - } - } + this.editToolbar = L.TB.bottomToolbar({ + mapElement: $(this.mapElement), + closeTitle: this.ctx.translate.instant('action.cancel'), + onClose: () => { + return this.deselectItem(true); + } + }); + + this.map.on('click', () => { + this.deselectItem(); + }); + + if (this.latestDataLayers.some(dl => dl.isEditable())) { + this.map.pm.setGlobalOptions({snappable: false}); + this.map.pm.applyGlobalOptions(); + } + + const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled()); + const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length; + const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled()); + + if (showDragModeButton || addSupportedDataLayers.length) { + const drawToolbar = L.TB.toolbar({ + position: this.settings.controlsPosition + }).addTo(this.map); + if (showDragModeButton) { + this.dragModeButton = drawToolbar.toolbarButton({ + id: 'dragMode', + title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'), + iconClass: 'tb-drag-mode', + click: (e, button) => { + this.toggleDragMode(e, button); + } + }); + } + this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers'); + if (this.addMarkerDataLayers.length) { + this.addMarkerButton = drawToolbar.toolbarButton({ + id: 'addMarker', + title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), + iconClass: 'tb-place-marker', + click: (e, button) => { + this.placeMarker(e, button); + } + }); + this.addMarkerButton.setDisabled(true); + this.setPlaceMarkerStyle(); + } + this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons'); + if (this.addPolygonDataLayers.length) { + this.addRectangleButton = drawToolbar.toolbarButton({ + id: 'addRectangle', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), + iconClass: 'tb-draw-rectangle', + click: (e, button) => { + this.drawRectangle(e, button); + } + }); + this.addRectangleButton.setDisabled(true); + this.addPolygonButton = drawToolbar.toolbarButton({ + id: 'addPolygon', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), + iconClass: 'tb-draw-polygon', + click: (e, button) => { + this.drawPolygon(e, button); + } + }); + this.addPolygonButton.setDisabled(true); + } + this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles'); + if (this.addCircleDataLayers.length) { + this.addCircleButton = drawToolbar.toolbarButton({ + id: 'addCircle', + title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), + iconClass: 'tb-draw-circle', + click: (e, button) => { + this.drawCircle(e, button); + } + }); + this.addCircleButton.setDisabled(true); + } + this.addPolylineDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polylines'); + if (this.addPolylineDataLayers.length) { + this.addPolylineButton = drawToolbar.toolbarButton({ + id: 'addPolyline', + title: this.ctx.translate.instant('widgets.maps.data-layer.polyline.draw-polyline'), + iconClass: 'tb-draw-polyline', + click: (e, button) => { + this.drawPolyline(e, button); + } + }); + this.addPolylineButton.setDisabled(true); + } + } } private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void { @@ -557,6 +579,13 @@ export abstract class TbMap { })); } + private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addPolylineDataLayers, (entity) => this.prepareDrawMode('Line', { + startPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + finishPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.finish-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + })); + } + private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[], prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { if (this.isPlacingItem) { @@ -678,6 +707,9 @@ export abstract class TbMap { case MapItemType.circle: this.createCircle(actionData); break; + case MapItemType.polyline: + this.createPolyline(actionData); + break; } } @@ -725,6 +757,17 @@ export abstract class TbMap { })); } + private createPolyline(actionData: PlaceMapItemActionData): void { + this.createItem(actionData, () => this.prepareDrawMode('Line', { + startPolyline: actionData.action.mapItemTooltips.startPolyline + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startPolyline) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startPolyline), + finishPolyline: actionData.action.mapItemTooltips.finishPolyline + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPolyline) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishPolyline), + })); + } + private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) { const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button'; if (this.createMapItemActionId === actionId) { @@ -765,7 +808,7 @@ export abstract class TbMap { this.createMapItemActionId = actionId; - const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => { + const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): { x: number; y: number } | TbPolygonRawCoordinates | TbCircleData => { switch (type) { case MapItemType.marker: if (layer instanceof L.Marker) { @@ -792,6 +835,15 @@ export abstract class TbMap { return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius()); } return null; + case MapItemType.polyline: + if (layer instanceof L.Polyline) { + let coordinates: any = layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0]; + } + return this.coordinatesToPolylineData(coordinates); + } + return null; } } } @@ -813,8 +865,8 @@ export abstract class TbMap { this.editToolbar.close(); } - private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record) { - this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en'); + private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record) { + this.map.pm.setLang('en', {tooltips: tooltipsTranslation}, 'en'); this.map.pm.enableDraw(shape); // @ts-ignore L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label'); @@ -848,46 +900,46 @@ export abstract class TbMap { tooltipData.instances = []; } $(root) - .find('a[role="button"]:not(.leaflet-pm-action)') - .each((_index, element) => { - let title: string; - if (element.title) { - title = element.title; - $(element).removeAttr('title'); - } else if (element.parentElement.title) { - title = element.parentElement.title; - $(element).parent().removeAttr('title'); - } - const tooltip = $(element).tooltipster( - { - content: title, - theme: 'tooltipster-shadow', - delay: 10, - triggerClose: { - click: true, - tap: true, - scroll: true, - mouseleave: true - }, - side, - distance: 2, - trackOrigin: true, - functionBefore: (_instance, helper) => { - if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { - return false; - } - }, - } - ); - const instance = tooltip.tooltipster('instance'); - tooltipData.instances.push(instance); - instance.on('destroyed', () => { - const index = tooltipData.instances.indexOf(instance); - if (index > -1) { - tooltipData.instances.splice(index, 1); + .find('a[role="button"]:not(.leaflet-pm-action)') + .each((_index, element) => { + let title: string; + if (element.title) { + title = element.title; + $(element).removeAttr('title'); + } else if (element.parentElement.title) { + title = element.parentElement.title; + $(element).parent().removeAttr('title'); } + const tooltip = $(element).tooltipster( + { + content: title, + theme: 'tooltipster-shadow', + delay: 10, + triggerClose: { + click: true, + tap: true, + scroll: true, + mouseleave: true + }, + side, + distance: 2, + trackOrigin: true, + functionBefore: (_instance, helper) => { + if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) { + return false; + } + }, + } + ); + const instance = tooltip.tooltipster('instance'); + tooltipData.instances.push(instance); + instance.on('destroyed', () => { + const index = tooltipData.instances.indexOf(instance); + if (index > -1) { + tooltipData.instances.splice(index, 1); + } + }); }); - }); }); } @@ -899,6 +951,7 @@ export abstract class TbMap { this.updateTripsAnchors(); this.updateBounds(); this.updateEditButtonsStates(); + } private updateTrips(subscription: IWidgetSubscription) { @@ -943,6 +996,7 @@ export abstract class TbMap { private updateTripsAppearance() { this.tripDataLayers.forEach(dl => dl.updateAppearance()); } + private updateTripsTime() { this.tripDataLayers.forEach(dl => dl.updateCurrentTime()); } @@ -995,8 +1049,7 @@ export abstract class TbMap { (!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds) && !mapBounds.contains(bounds) ) - ) - { + ) { this.bounds = bounds; if (!this.ignoreUpdateBounds && !this.isPlacingItem) { this.fitBounds(bounds); @@ -1023,6 +1076,9 @@ export abstract class TbMap { if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) { this.addCircleButton.setDisabled(true); } + if (this.addPolylineButton && this.addPolylineButton !== this.currentEditButton) { + this.addPolylineButton.setDisabled(true); + } this.customActionsToolbar?.setDisabled(true); } else { if (this.dragModeButton) { @@ -1040,6 +1096,9 @@ export abstract class TbMap { if (this.addCircleButton) { this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } + if (this.addPolylineButton) { + this.addPolylineButton.setDisabled(!this.addPolylineDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } this.customActionsToolbar?.setDisabled(false); } } @@ -1150,7 +1209,7 @@ export abstract class TbMap { $event.preventDefault(); $event.stopPropagation(); } - const { entityId, entityName, entityLabel, entityType } = data.$datasource; + const {entityId, entityName, entityLabel, entityType} = data.$datasource; this.ctx.actionsApi.handleWidgetAction($event, action, { entityType, id: entityId @@ -1219,7 +1278,7 @@ export abstract class TbMap { } } - public saveLocation(data: FormattedData, values: {[key: string]: any}): Observable { + public saveLocation(data: FormattedData, values: { [key: string]: any }): Observable { const datasource = data.$datasource; let dataKeys = datasource.dataKeys; if (datasource.latestDataKeys) { @@ -1329,18 +1388,21 @@ export abstract class TbMap { } } - public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng; + public abstract locationDataToLatLng(position: { x: number; y: number }): L.LatLng; - public abstract latLngToLocationData(position: L.LatLng): {x: number; y: number}; + public abstract latLngToLocationData(position: L.LatLng): { x: number; y: number }; public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates; public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates; + public abstract polylineDataToCoordinates(coordinates: TbPolylineRawCoordinates): TbPolylineRawCoordinates; + + public abstract coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates; + public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData; public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html index bc3ad72e58..fe53ef9f66 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.html @@ -20,7 +20,7 @@
widgets.input-widgets.no-image - last photo + last photo
+ +
widgets.maps.data-layer.shape.stroke
@@ -455,7 +477,9 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"> + helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : + (dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}" + formControlName="strokeColor">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 48854adc44..af653308da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -29,7 +29,7 @@ import { MarkerType, pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, - PolygonsDataLayerSettings, + PolygonsDataLayerSettings, PolylinesDataLayerSettings, ShapeDataLayerSettings, ShapeFillType, TripsDataLayerSettings, updateDataKeyToNewDsType @@ -113,6 +113,8 @@ export class MapDataLayerDialogComponent extends DialogComponent this.updateValidators() @@ -291,6 +298,15 @@ export class MapDataLayerDialogComponent extends DialogComponent + + +
-
-
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index 747ef21681..06e34e4eea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -38,6 +38,7 @@ import { } from '@shared/models/widget/maps/map.models'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'tb-map-data-layers', @@ -79,6 +80,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val noDataLayersText: string; + get dragEnabled(): boolean { + return this.dataLayersFormArray().controls.length > 1; + } + private propagateChange = (_val: any) => {}; constructor(private mapSettingsComponent: MapSettingsComponent, @@ -104,6 +109,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; break; + case 'polylines': + this.addDataLayerText = 'widgets.maps.data-layer.polyline.add-polylines'; + this.noDataLayersText = 'widgets.maps.data-layer.polyline.no-polylines'; + break; } this.dataLayersFormGroup = this.fb.group({ dataLayers: [this.fb.array([]), []] @@ -162,6 +171,13 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val removeDataLayer(index: number) { (this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index); } + + layerDrop(event: CdkDragDrop) { + const layersArray = this.dataLayersFormArray(); + const layer = layersArray.at(event.previousIndex); + layersArray.removeAt(event.previousIndex); + layersArray.insert(event.currentIndex, layer); + } addDataLayer() { const dataLayer = mergeDeep({} as MapDataLayerSettings, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index fe2afa3b19..c32242cc32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -45,6 +45,8 @@ {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }} + //todo translation + Polylines
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 5b949bae75..ab32155420 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -145,6 +145,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid markers: [null, []], polygons: [null, []], circles: [null, []], + polylines: [null, []], additionalDataSources: [null, []], controlsPosition: [null, []], zoomActions: [null, []], @@ -180,7 +181,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid }); merge(this.mapSettingsFormGroup.get('markers').valueChanges, this.mapSettingsFormGroup.get('polygons').valueChanges, - this.mapSettingsFormGroup.get('circles').valueChanges + this.mapSettingsFormGroup.get('circles').valueChanges, + this.mapSettingsFormGroup.get('polylines').valueChanges ).pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -281,6 +283,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); } + if (!dragModeButtonSettingsEnabled) { + const polylines: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polylines').value; + dragModeButtonSettingsEnabled = polylines.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + } if (!dragModeButtonSettingsEnabled) { const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html index 2225e22a44..cabd91d4e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html @@ -15,19 +15,32 @@ limitations under the License. --> -
-
- widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - -
-
- widgets.input-widgets.image-settings -
- - widgets.input-widgets.image-format +
+
+
widgets.input-widgets.general-settings
+
+
widgets.input-widgets.widget-title
+ + + +
+
+
+
widgets.input-widgets.save-image
+ + {{ 'widgets.input-widgets.save-to-gallery' | translate }} + + @if (photoCameraInputWidgetSettingsForm.get('saveToGallery').value) { + + {{ 'widgets.input-widgets.public-image' | translate }} + + } +
+
+
widgets.input-widgets.image-settings
+
+
widgets.input-widgets.image-format
+ {{ 'widgets.input-widgets.image-format-jpeg' | translate }} @@ -40,20 +53,33 @@ - - widgets.input-widgets.image-quality - - -
-
- - widgets.input-widgets.max-image-width - - - - widgets.input-widgets.max-image-height - - -
-
+
+ + @if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') { +
+
widgets.input-widgets.image-quality
+ + + % + +
+ } + +
+
Size
+
+
widgets.input-widgets.max-image-width
+ + + px + + +
widgets.input-widgets.max-image-height
+ + + px + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts index 30c3e39833..4e7d552f44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts @@ -15,10 +15,10 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; @Component({ selector: 'tb-photo-camera-input-widget-settings', @@ -42,6 +42,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo return { widgetTitle: '', + saveToGallery: false, + usePublicGalleryLink: true, imageFormat: 'image/png', imageQuality: 0.92, maxWidth: 640, @@ -57,11 +59,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo widgetTitle: [settings.widgetTitle, []], // Image settings - + saveToGallery: [settings.saveToGallery], + usePublicGalleryLink: [settings.usePublicGalleryLink], imageFormat: [settings.imageFormat, []], - imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]], + imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(100)]], maxWidth: [settings.maxWidth, [Validators.min(1)]], maxHeight: [settings.maxHeight, [Validators.min(1)]] }); } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + saveToGallery: settings.saveToGallery ?? false, + usePublicGalleryLink: settings.usePublicGalleryLink ?? false, + imageQuality: settings.imageQuality * 100 + } + } + + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + imageQuality: settings.imageQuality / 100 + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 7e91a2f043..8f36242d95 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -451,107 +451,36 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - - const cssString = - '.mat-mdc-input-element::placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-moz-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-webkit-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element:-ms-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar:not([color="primary"]) button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-tab .mdc-tab__text-label {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDarkDisabled2 + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-row {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-cell {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' + - 'border-bottom-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background, ' + - '.mat-table .mat-header-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background {\n' + - 'border-color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'transition: background-color .2s;\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity {\n' + - 'background-color: ' + currentEntityColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + currentEntityStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) {\n' + - 'background-color: ' + hoverColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + hoverStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n' + - 'background-color: ' + selectedColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + selectedStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky, .mat-mdc-table .mat-mdc-header-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row {\n' + - 'color: ' + mdDark + ';\n' + - 'background-color: rgba(0, 0, 0, 0);\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button tb-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] tb-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-divider {\n' + - 'border-top-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-paginator {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button[disabled][disabled] {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-paginator .mat-mdc-select-value {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}'; + + const cssString = ` { + --mat-toolbar-container-text-color: ${mdDark}; + --mat-tab-header-active-label-text-color: ${mdDark}; + --mat-tab-header-inactive-label-text-color: ${mdDark}; + --mat-tab-header-pagination-icon-color: ${mdDark}; + --mat-tab-header-pagination-disabled-icon-color: ${mdDarkDisabled2}; + --mat-table-header-headline-color: ${mdDarkSecondary}; + --mat-table-row-item-label-text-color: ${mdDark}; + --mat-icon-color: ${mdDarkSecondary}; + --mdc-icon-button-disabled-icon-color: ${mdDarkDisabled}; + --mat-divider-color: ${mdDarkDivider}; + --mat-paginator-container-text-color: ${mdDarkSecondary}; + --mdc-icon-button-icon-color: ${mdDarkSecondary}; + --mat-paginator-enabled-icon-color: ${mdDarkSecondary}; + --mat-paginator-disabled-icon-color: ${mdDarkDisabled}; + --mat-select-enabled-trigger-text-color: ${mdDarkSecondary}; + --mat-select-disabled-trigger-text-color: ${mdDarkDisabled}; + --mat-table-row-item-outline-color: ${mdDarkDivider}; + --mdc-checkbox-unselected-focus-icon-color: ${mdDarkSecondary}; + + --tb-orig-background-color: ${origBackgroundColor}; + --tb-current-entity-color: ${currentEntityColor}; + --tb-current-entity-sticky-color: ${currentEntityStickyColor}; + --tb-hover-color: ${hoverColor}; + --tb-hover-sticky-color: ${hoverStickyColor}; + --tb-selected-color: ${selectedColor}; + --tb-selected-sticky-color: ${selectedStickyColor}; + } + `; return cssString; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss index 3df2c7d0a2..dd8a026db4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss @@ -44,7 +44,53 @@ padding: 0 5px; } } + + .mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron { + border-color: var(--mat-tab-header-pagination-disabled-icon-color); + } + } + + .mat-mdc-input-element::placeholder { + color: var(--mat-table-header-headline-color); } + + .mat-mdc-table { + .mat-mdc-header-row { + background-color: var(--tb-orig-background-color); + } + + .mat-mdc-cell, .mat-mdc-header-cell { + &.mat-mdc-table-sticky { + background-color:var(--tb-orig-background-color); + } + } + + .mat-mdc-row { + background-color: rgba(0,0,0,0); + &.tb-current-entity { + background-color: var(--tb-current-entity-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-current-entity-sticky-color); + } + } + &:hover:not(.tb-current-entity){ + background-color: var(--tb-hover-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-hover-sticky-color); + } + } + &.mat-row-select.mat-selected:not(.tb-current-entity){ + background-color: var(--tb-selected-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-selected-sticky-color); + } + } + &, .mat-mdc-cell.mat-mdc-table-sticky { + transition: background-color .2s; + background-color:var(--tb-orig-background-color); + } + } + } } :host-context(.tb-has-timewindow) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 73ea953a15..73ab2e679c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -41,7 +41,7 @@ class="flex-1" mat-stretch-tabs="false" [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> - +
):string { + if (entityLabelCache.has(source.entityId)) { + return entityLabelCache.get(source.entityId); + } + + const value = this.useEntityLabel + ? (source.entityLabel || source.entityName) + : source.entityName; + + const translated = this.utils.customTranslation(value); + entityLabelCache.set(source.entityId, translated); + return translated; + } + + private sortDatasources(source: Datasource[], entityLabelCache:Map): Datasource[] { + source.forEach(ds => this.getTabLabel(ds, entityLabelCache)); + + if (this.settings.tabSortKey === TabSortKey.TIMESTAMP) { + return source; } + return source.sort((a, b) => { + const valueA = entityLabelCache.get(a.entityId); + const valueB = entityLabelCache.get(b.entityId); + return this.settings.tabSortKey === TabSortKey.NAME_ASC + ? valueA.localeCompare(valueB) + : valueB.localeCompare(valueA); + }); } private updateDatasources() { @@ -405,9 +434,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.sourceIndex = 0; let keyOffset = 0; let latestKeyOffset = 0; + const entityLabelCache = new Map(); const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; if (this.datasources) { - for (const datasource of this.datasources) { + const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache); + for (const datasource of sortedDatasources) { const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); const source = {} as TimeseriesTableSource; source.header = this.prepareHeader(datasource); @@ -424,6 +455,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI source.pageLink = new PageLink(pageSize, 0, null, sortOrder); source.rowDataTemplate = {}; source.rowDataTemplate.Timestamp = null; + source.displayName = entityLabelCache.get(datasource.entityId); if (this.showTimestamp) { source.displayedColumns.push('0'); } diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index 48b6105bf9..3d4e35f1d0 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -62,6 +62,7 @@ placeholder="{{ 'common.enter-search' | translate }}"/> + - } @else if (keyControl.hasError('required') && keyControl.touched) { + } @else if (keyControl.hasError('required') && keyControl.touched && keyControl.enabled) { { this.cachedResult = result; @@ -152,10 +152,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val registerOnTouched(_): void {} validate(): ValidationErrors | null { - return this.keyControl.valid ? null : { keyControl: false }; + return this.keyControl.valid || this.keyControl.disabled ? null : { keyControl: false }; } writeValue(value: string): void { this.keyControl.patchValue(value, {emitEvent: false}); } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.keyControl.disable({emitEvent: false}); + } else { + this.keyControl.enable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html index 6635921d7f..fb2888c636 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -47,7 +47,7 @@ (optionSelected)="selected($event)" [displayWith]="displayEntitySubtypeFn"> - +
diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 105257dd24..1f46ea1e34 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -39,11 +39,13 @@ [flow]="flow.flowJs">
cloud_upload - {{ dropLabel }} - +
+ {{ dropLabel }} + +
diff --git a/ui-ngx/src/app/shared/components/phone-input.component.html b/ui-ngx/src/app/shared/components/phone-input.component.html index 1b025e8113..ff5d5f4853 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.html +++ b/ui-ngx/src/app/shared/components/phone-input.component.html @@ -27,7 +27,7 @@
- + {{ label }} + subscriptSizing="dynamic"> @if (labelText && !inlineField) { {{ labelText }} } @@ -41,7 +41,9 @@ {{ hasError }} - @if (!inlineField) { diff --git a/ui-ngx/src/app/shared/components/time-unit-input.component.ts b/ui-ngx/src/app/shared/components/time-unit-input.component.ts index 44f1be514a..678d9e9f00 100644 --- a/ui-ngx/src/app/shared/components/time-unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.ts @@ -178,7 +178,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, this.timeInputForm.disable({emitEvent: false}); } else { this.timeInputForm.enable({emitEvent: false}); - if(this.timeInputForm.invalid) { + if(!this.timeInputForm.valid) { setTimeout(() => this.updatedModel(this.timeInputForm.value, true)) } } @@ -200,7 +200,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } validate(): ValidationErrors | null { - return this.timeInputForm.valid ? null : { + return this.timeInputForm.disabled || this.timeInputForm.valid ? null : { timeInput: false }; } diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts index a6d6d5e87b..6bcf3cc0df 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts @@ -35,7 +35,7 @@ import { BranchInfo } from '@shared/models/vc.models'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { isNotEmptyStr } from '@core/utils'; import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; -import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; @Component({ selector: 'tb-branch-autocomplete', diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts new file mode 100644 index 0000000000..fafee4e97b --- /dev/null +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -0,0 +1,152 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { CustomTimeSchedulerItem } from "@shared/models/device.models"; +import { DashboardId } from "@shared/models/id/dashboard-id"; +import { TimeUnit } from "@shared/models/time/time.models"; +import { + BooleanOperation, + ComplexOperation, + EntityKeyValueType, + FilterPredicateType, + NumericOperation, + StringOperation +} from "@shared/models/query/query.models"; + +export enum AlarmRuleScheduleType { + ANY_TIME = 'ANY_TIME', + SPECIFIC_TIME = 'SPECIFIC_TIME', + CUSTOM = 'CUSTOM' +} + +export const AlarmRuleScheduleTypeTranslationMap = new Map( + [ + [AlarmRuleScheduleType.ANY_TIME, 'alarm-rule.schedule.any-time'], + [AlarmRuleScheduleType.SPECIFIC_TIME, 'alarm-rule.schedule.specific-time'], + [AlarmRuleScheduleType.CUSTOM, 'alarm-rule.schedule.custom'] + ] +); + +export enum AlarmRuleConditionType { + SIMPLE = 'SIMPLE', + DURATION = 'DURATION', + REPEATING = 'REPEATING' +} + +export const AlarmRuleConditionTypeTranslationMap = new Map( + [ + [AlarmRuleConditionType.SIMPLE, 'alarm-rule.conditions.simple'], + [AlarmRuleConditionType.DURATION, 'alarm-rule.conditions.duration'], + [AlarmRuleConditionType.REPEATING, 'alarm-rule.conditions.repeating'] + ] +); + +export enum AlarmRuleExpressionType { + SIMPLE = 'SIMPLE', + TBEL = 'TBEL', +} + +export const FilterPredicateTypeTranslationMap = new Map( + [ + [FilterPredicateType.STRING, 'alarm-rule.filter-predicate-type.string'], + [FilterPredicateType.NUMERIC, 'alarm-rule.filter-predicate-type.numeric'], + [FilterPredicateType.BOOLEAN, 'alarm-rule.filter-predicate-type.boolean'], + [FilterPredicateType.COMPLEX, 'alarm-rule.filter-predicate-type.complex'] + ] +); + +export interface AlarmRule { + condition: AlarmRuleCondition; + alarmDetails?: string; + dashboardId?: DashboardId; +} + +export interface AlarmRuleCondition { + type: AlarmRuleConditionType; + expression: AlarmRuleExpression; + schedule?: AlarmRuleSchedule; + unit?: TimeUnit; + value?: AlarmRuleValue; + count?: AlarmRuleValue; +} + +export interface AlarmRuleExpression { + type: AlarmRuleExpressionType; + expression?: string; + filters?: Array; + operation?: ComplexOperation; +} + +export interface AlarmRuleSchedule { + staticValue?: { + type?: AlarmRuleScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; + }; + dynamicValueArgument?: string; +} + +export interface AlarmRuleFilter { + argument: string; + valueType: EntityKeyValueType; + operation: ComplexOperation; + predicates: AlarmRuleFilterPredicate[]; +} + +export interface AlarmRulePredicateInfo { + keyFilterPredicate: AlarmRuleFilterPredicate; +} + +export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | + NumericAlarmRuleFilterPredicate | + BooleanAlarmRuleFilterPredicate | + ComplexAlarmRuleFilterPredicate; + +export interface AlarmRuleValue { + dynamicValueArgument?: string; + staticValue?: T +} + +export interface StringAlarmRuleFilterPredicate { + type: FilterPredicateType.STRING; + operation: StringOperation; + value: AlarmRuleValue; + ignoreCase: boolean; +} + +export interface NumericAlarmRuleFilterPredicate { + type: FilterPredicateType.NUMERIC; + operation: NumericOperation; + value: AlarmRuleValue; +} + +export interface BooleanAlarmRuleFilterPredicate { + type: FilterPredicateType.BOOLEAN; + operation: BooleanOperation; + value: AlarmRuleValue; +} + +export interface BaseComplexFilterPredicate { + type: FilterPredicateType.COMPLEX; + operation: ComplexOperation; + predicates: Array; +} + +export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate; diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 841baea168..1b43700ebe 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -30,40 +30,169 @@ import { endGroupHighlightRule } from '@shared/models/ace/ace.models'; import { EntitySearchDirection } from '@shared/models/relation.models'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { AlarmSeverity } from "@shared/models/alarm.models"; -export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { - configuration: CalculatedFieldConfiguration; - type: CalculatedFieldType; +interface BaseCalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { entityId: EntityId; } +export interface CalculatedFieldSimple extends BaseCalculatedField { + type: CalculatedFieldType.SIMPLE; + configuration: CalculatedFieldSimpleConfiguration; +} + +export interface CalculatedFieldScript extends BaseCalculatedField { + type: CalculatedFieldType.SCRIPT; + configuration: CalculatedFieldScriptConfiguration; +} + +export interface CalculatedFieldGeofencing extends BaseCalculatedField { + type: CalculatedFieldType.GEOFENCING; + configuration: CalculatedFieldGeofencingConfiguration; +} + +export interface CalculatedFieldPropagation extends BaseCalculatedField { + type: CalculatedFieldType.PROPAGATION; + configuration: CalculatedFieldPropagationConfiguration; +} + +export interface CalculatedFieldAlarmRule extends BaseCalculatedField { + type: CalculatedFieldType.ALARM; + configuration: CalculatedFieldAlarmRuleConfiguration; +} + +export type CalculatedField = + | CalculatedFieldSimple + | CalculatedFieldScript + | CalculatedFieldGeofencing + | CalculatedFieldPropagation + | CalculatedFieldAlarmRule; + export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', - GEOFENCING = 'GEOFENCING' + GEOFENCING = 'GEOFENCING', + PROPAGATION = 'PROPAGATION', + RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION', + ALARM = 'ALARM', } -export const CalculatedFieldTypeTranslations = new Map( +interface CalculatedFieldTypeTranslate { + name: string; + hint?: string; +} + +export const CalculatedFieldTypeTranslations = new Map( [ - [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], - [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], - [CalculatedFieldType.GEOFENCING, 'calculated-fields.type.geofencing'], + [CalculatedFieldType.SIMPLE, { + name: 'calculated-fields.type.simple' + }], + [CalculatedFieldType.SCRIPT, { + name: 'calculated-fields.type.script' + }], + [CalculatedFieldType.GEOFENCING, { + name: 'calculated-fields.type.geofencing' + }], + [CalculatedFieldType.PROPAGATION, { + name: 'calculated-fields.type.propagation' + }], + [CalculatedFieldType.RELATED_ENTITIES_AGGREGATION, { + name: 'calculated-fields.type.related-entities-aggregation', + hint: 'calculated-fields.type.related-entities-aggregation-hint' + }], ] ) -export interface CalculatedFieldConfiguration { - type: CalculatedFieldType; - expression?: string; - arguments?: Record; - zoneGroups?: Record; +export type CalculatedFieldConfiguration = + | CalculatedFieldSimpleConfiguration + | CalculatedFieldScriptConfiguration + | CalculatedFieldGeofencingConfiguration + | CalculatedFieldPropagationConfiguration + | CalculatedFieldRelatedAggregationConfiguration + | CalculatedFieldAlarmRuleConfiguration; + +export interface CalculatedFieldSimpleConfiguration { + type: CalculatedFieldType.SIMPLE; + expression: string; + arguments: Record; + useLatestTs: boolean; + output: CalculatedFieldSimpleOutput; +} + +export interface CalculatedFieldScriptConfiguration { + type: CalculatedFieldType.SCRIPT; + expression: string; + arguments: Record; + output: CalculatedFieldOutput; +} + +export interface CalculatedFieldGeofencingConfiguration { + type: CalculatedFieldType.GEOFENCING; + zoneGroups: Record; + scheduledUpdateEnabled: boolean; + scheduledUpdateInterval?: number; + output: CalculatedFieldOutput; +} + +export interface CalculatedFieldRelatedAggregationConfiguration { + type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + relation: RelationPathLevel; + arguments: Record; + metrics: Record; + deduplicationIntervalInSec: number; scheduledUpdateInterval?: number; + useLatestTs: boolean; + output: CalculatedFieldOutput & { decimalsByDefault?: number; }; +} + +interface BasePropagationConfiguration { + type: CalculatedFieldType.PROPAGATION; + relation: RelationPathLevel; + arguments: Record; output: CalculatedFieldOutput; } -export interface CalculatedFieldOutput { - type: OutputType; +interface CalculatedFieldAlarmRuleConfiguration { + type: CalculatedFieldType.ALARM; + arguments: Record; + createRules: Record; + clearRule?: AlarmRule; + propagate: boolean; + propagateToOwner: boolean; + propagateToTenant: boolean; + propagateRelationTypes?: Array; +} + +export interface PropagationWithNoExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: false; +} + +export interface PropagationWithExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: true; + expression: string; +} + +export type CalculatedFieldPropagationConfiguration = + | PropagationWithNoExpression + | PropagationWithExpression; + +export type CalculatedFieldOutput = + | CalculatedFieldOutputAttribute + | CalculatedFieldOutputTimeSeries; + +export interface CalculatedFieldOutputAttribute { + type: OutputType.Attribute, + scope: AttributeScope; +} + +export interface CalculatedFieldOutputTimeSeries { + type: OutputType.Timeseries; +} + +export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { name: string; - scope?: AttributeScope; decimalsByDefault?: number; } @@ -73,6 +202,7 @@ export enum ArgumentEntityType { Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', + Owner = 'CURRENT_OWNER', RelationQuery = 'RELATION_PATH_QUERY', } @@ -83,6 +213,7 @@ export const ArgumentEntityTypeTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down-child'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up-parent'], + ] +) + export enum ArgumentType { Attribute = 'ATTRIBUTE', LatestTelemetry = 'TS_LATEST', @@ -158,23 +296,76 @@ export interface CalculatedFieldArgument { refEntityKey: RefEntityKey; defaultValue?: string; refEntityId?: RefEntityId; + refDynamicSourceConfiguration?: RefDynamicSourceConfiguration; limit?: number; timeWindow?: number; } +export interface RefDynamicSourceConfiguration { + type: ArgumentEntityType.Owner; +} + +export enum AggFunction { + AVG='AVG', + MIN='MIN', + MAX='MAX', + SUM='SUM', + COUNT='COUNT', + COUNT_UNIQUE='COUNT_UNIQUE' +} + +export const AggFunctionTranslations = new Map([ + [AggFunction.AVG, 'calculated-fields.metrics.aggregation-type.avg'], + [AggFunction.MIN, 'calculated-fields.metrics.aggregation-type.min'], + [AggFunction.MAX, 'calculated-fields.metrics.aggregation-type.max'], + [AggFunction.SUM, 'calculated-fields.metrics.aggregation-type.sum'], + [AggFunction.COUNT, 'calculated-fields.metrics.aggregation-type.count'], + [AggFunction.COUNT_UNIQUE, 'calculated-fields.metrics.aggregation-type.count-unique'], +]) + +export interface CalculatedFieldAggMetric { + function: AggFunction; + filter?: string; + input: AggKeyInput | AggFunctionInput; +} + +export interface CalculatedFieldAggMetricValue extends CalculatedFieldAggMetric { + name: string; +} + +export enum AggInputType { + key = 'key', + function = 'function' +} + +export const AggInputTypeTranslations = new Map([ + [AggInputType.key, 'calculated-fields.metrics.value-source-type.key'], + [AggInputType.function, 'calculated-fields.metrics.value-source-type.function'], +]) + +export interface AggKeyInput { + type: AggInputType.key; + key: string; +} + +export interface AggFunctionInput { + type: AggInputType.function; + function: string; +} + export interface CalculatedFieldGeofencing { perimeterKeyName: string; reportStrategy: GeofencingReportStrategy; refEntityId?: RefEntityId; - refDynamicSourceConfiguration: RefDynamicSourceConfiguration; + refDynamicSourceConfiguration: RefDynamicSourceGeofencingConfiguration; createRelationsWithMatchedZones: boolean; relationType: string; direction: EntitySearchDirection; } -export interface RefDynamicSourceConfiguration { - type?: ArgumentEntityType.RelationQuery; - levels?: Array<{direction: EntitySearchDirection; relationType: string;}>; +export interface RefDynamicSourceGeofencingConfiguration { + type: ArgumentEntityType.RelationQuery | ArgumentEntityType.Owner; + levels?: Array; } export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { @@ -241,6 +432,11 @@ export interface CalculatedFieldArgumentValueBase { type: ArgumentType; } +export interface RelationPathLevel { + direction: EntitySearchDirection; + relationType: string; +} + export interface CalculatedFieldAttributeArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; @@ -705,3 +901,13 @@ export const calculatedFieldDefaultScript = 'return {\n' + ' "temperatureC": (temperatureF - 32) / 1.8\n' + '};' + +export function notEmptyObjectValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) { + return {emptyObject: true}; + } + return null; + }; +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 059fe39ada..34023f939b 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -19,6 +19,11 @@ import { TenantId } from './id/tenant-id'; import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { QueueInfo } from '@shared/models/queue.models'; +import { FormControl } from '@angular/forms'; + +export type FormControlsFrom = { + [K in keyof T]-?: FormControl; +}; export enum TenantProfileType { DEFAULT = 'DEFAULT' @@ -101,6 +106,10 @@ export interface DefaultTenantProfileConfiguration { maxCalculatedFieldsPerEntity: number; maxArgumentsPerCF: number; + maxRelationLevelPerCfArgument: number; + minAllowedDeduplicationIntervalInSecForCF: number; + maxRelatedEntitiesToReturnPerCfArgument: number; + minAllowedScheduledUpdateIntervalInSecForCF: number; maxDataPointsPerRollingArg: number; maxStateSizeInKBytes: number; maxSingleValueArgumentSizeInKBytes: number; @@ -165,6 +174,10 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxCalculatedFieldsPerEntity: 5, maxArgumentsPerCF: 10, maxDataPointsPerRollingArg: 1000, + maxRelationLevelPerCfArgument: 10, + minAllowedDeduplicationIntervalInSecForCF: 60, + maxRelatedEntitiesToReturnPerCfArgument: 100, + minAllowedScheduledUpdateIntervalInSecForCF: 0, maxStateSizeInKBytes: 32, maxSingleValueArgumentSizeInKBytes: 2, calculatedFieldDebugEventsRateLimit: '' diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 01ac1b2aaa..f612dd1503 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -659,7 +659,8 @@ export enum MapItemType { marker = 'marker', polygon = 'polygon', rectangle = 'rectangle', - circle = 'circle' + circle = 'circle', + polyline = 'polyline' } export const widgetActionTypes = Object.keys(WidgetActionType) @@ -699,6 +700,7 @@ export const mapItemTypeTranslationMap = new Map( [ MapItemType.polygon, 'widget-action.map-item.polygon' ], [ MapItemType.rectangle, 'widget-action.map-item.rectangle' ], [ MapItemType.circle, 'widget-action.map-item.circle' ], + [ MapItemType.polyline, 'widget-action.map-item.polyline' ] ] ) @@ -826,6 +828,8 @@ export interface MapItemTooltips { finishRect?: string; startCircle?: string; finishCircle?: string; + startPolyline?: string; + finishPolyline?: string; } export const mapItemTooltipsTranslation: Required = Object.freeze({ @@ -836,7 +840,9 @@ export const mapItemTooltipsTranslation: Required = Object.free startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint', startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint', - finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint' + finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint', + startPolyline: 'widgets.maps.data-layer.polyline.polyline-place-first-point-hint', + finishPolyline: 'widgets.maps.data-layer.polyline.finish-polyline-hint' }) export interface WidgetActionDescriptor extends WidgetAction { @@ -1145,5 +1151,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { } - } diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 8df70edae3..d3be99541f 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -79,7 +79,7 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting }; const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, - dataLayerType: MapDataLayerType): DataKey[] => { + dataLayerType: MapDataLayerType): DataKey[] => { const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; switch (dataLayerType) { case 'trips': @@ -96,6 +96,9 @@ const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, case 'circles': dataKeys.push((settings as CirclesDataLayerSettings).circleKey); break; + case 'polylines': + dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey); + break; } return dataKeys; }; @@ -159,6 +162,7 @@ export interface DataLayerEditSettings { snappable: boolean; } + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataSources?: MapDataSourceSettings[]; additionalDataKeys?: DataKey[]; @@ -166,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { tooltip: DataLayerTooltipSettings; click: WidgetAction; groups?: string[]; - edit: DataLayerEditSettings; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -182,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details' - : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 }, @@ -196,9 +200,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { @@ -222,7 +226,7 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData case 'markers': const markersDataLayer = dataLayer as MarkersDataLayerSettings; if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name || - !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { + !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { return false; } break; @@ -232,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData return false; } break; + case 'polylines': + const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings; + if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) { + return false; + } + break; case 'circles': const circlesDataLayer = dataLayer as CirclesDataLayerSettings; if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) { @@ -300,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings { iconContainer?: MarkerIconContainer; icon: string; } + export interface MarkerClusteringSettings { enable: boolean; zoomOnClick: boolean; @@ -600,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial< color: '#3388ff', }, strokeWeight: 3 -} as Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; @@ -651,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) + +export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings { + polylineKey: DataKey; +} + +export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnly = false): PolylinesDataLayerSettings => mergeDeep({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First polyline' : '', + polylineKey: { + name: functionsOnly ? 'f(x)' : 'perimeter', + label: 'perimeter', + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + settings: {}, + color: materialColors[0].value + } +} as PolylinesDataLayerSettings, defaultBasePolylinesDataLayerSettings(mapType) as PolylinesDataLayerSettings); + +export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, + fillColor: { + type: DataLayerColorType.constant, + color: 'rgba(51,136,255,0.2)', + }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) + export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { switch (dataLayerType) { @@ -664,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map return defaultPolygonsDataLayerSettings(mapType, functionsOnly); case 'circles': return defaultCirclesDataLayerSettings(mapType, functionsOnly); + case 'polylines': + return defaultPolylinesDataLayerSettings(mapType, functionsOnly); } }; @@ -677,6 +730,8 @@ export const defaultBaseMapDataLayerSettings = ( return defaultBasePolygonsDataLayerSettings(mapType) as T; case 'circles': return defaultBaseCirclesDataLayerSettings(mapType) as T; + case 'polylines': + return defaultBasePolylinesDataLayerSettings(mapType) as T; } } @@ -727,13 +782,13 @@ export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSource }; export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const dataSource: AdditionalMapDataSourceSettings = control.value; - if (!additionalMapDataSourceValid(dataSource)) { - return { - dataSource: true - }; - } - return null; + const dataSource: AdditionalMapDataSourceSettings = control.value; + if (!additionalMapDataSourceValid(dataSource)) { + return { + dataSource: true + }; + } + return null; }; export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => { @@ -815,6 +870,7 @@ export interface BaseMapSettings { markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; + polylines: PolylinesDataLayerSettings[]; additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; @@ -839,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { markers: [], polygons: [], circles: [], + polylines: [], additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], @@ -1245,6 +1302,12 @@ export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][] export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; export type TbPolygonCoordinates = TbPolygonCoordinate[]; +export type TbPolylineRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; +export type TbPolylineRawCoordinates = TbPolylineRawCoordinate[]; +export type TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; +export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolylineCoordinates = TbPolylineCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 3e414577ec..b94e72d76f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -237,6 +237,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @NgModule({ providers: [ DatePipe, + SelectableColumnsPipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, HighlightPipe, diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 92c9bdb054..994b431ade 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -98,7 +98,8 @@ "settings": { "showTimestamp": true, "displayPagination": true, - "defaultPageSize": 10 + "defaultPageSize": 10, + "tabSortKey": "NAME_ASC" }, "title": "{i18n:api-usage.exceptions}", "dropShadow": true, diff --git a/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md new file mode 100644 index 0000000000..94ffabbc34 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md @@ -0,0 +1,123 @@ +#### Active all time schedule format + +An attribute with a dynamic value for an active all-time schedule format must contain an empty JSON object or JSON in the following format: + +```javascript +{ + "type": "ANY_TIME" +} +``` + +#### Specific time schedule format + +An attribute with a dynamic value for a specific schedule format must have JSON in the following format: + +```javascript +{ + "type": "SPECIFIC_TIME", + "daysOfWeek": [ + 2, + 4 + ], + "endsOn": 0, + "startsOn": 0, + "timezone": "Europe/Kiev" +} +``` + +
    +
  • +timezone: this value is used to designate the timezone you are using. +
  • +
  • +daysOfWeek: this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. +
  • +
  • +startsOn: this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days. +
  • +
  • +endsOn: this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days. +
  • +
+When startsOn and endsOn equals 0 it's means that the schedule will be active the whole day. + +#### Custom time schedule format + +An attribute with a dynamic value for a custom schedule format must have JSON in the following format: + +```javascript +{ + "type": "CUSTOM" + "timezone": "Europe/Kiev", + "items": [ + { + "dayOfWeek": 1, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 2, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 3, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 4, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 5, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 6, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 7, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + } + ] +} +``` + +
    +
  • +timezone: this value is used to designate the timezone you are using. +
  • +
  • +items: the array of values representing the days on which the schedule will be active. +
  • +
+ +One array item contains such fields: +
    +
  • +dayOfWeek: this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. +
  • +
  • +enabled: this boolean value, used to designate that the specified day in the schedule will be enabled. +
  • +
  • +startsOn: this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day. +
  • +
  • +endsOn: this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day. +
  • +
+When startsOn and endsOn equals 0 it's means that the schedule will be active the whole day. diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md new file mode 100644 index 0000000000..ce8ca22f51 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md @@ -0,0 +1,10 @@ +## Calculated Field TBEL Filter Function + +The **filter()** 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 `latestTs` and provides access to all arguments. + +### Function Signature + +```javascript +function calculate(ctx, arg1, arg2, ...): boolean +``` diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm.md b/ui-ngx/src/assets/help/en_US/notification/alarm.md index b03addf7a1..e0e0dc3e8c 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm.md @@ -22,7 +22,7 @@ Available template parameters: * `recipientEmail` - email of the recipient; * `recipientFirstName` - first name of the recipient; * `recipientLastName` - last name of the recipient; -* `info.` - any key field from the alarm's additional info. Fox example, if additional info is `{"data": "Temperature is 25"}`, use `${info.data}` to access "Temperature is 25"; +* `details.` - any key field from the alarm's details. Fox example, if details are `{"data": "Temperature is 25"}`, use `${details.data}` to access "Temperature is 25"; Parameter names must be wrapped using `${...}`. For example: `${action}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md index bc8823c777..3bb410f10b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md @@ -79,7 +79,7 @@ function AddEntityDialogController(instance) { const mapType = widgetContext.mapInstance.type(); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); - } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line') { attributes.push({key: 'perimeter', value: additionalParams.coordinates}); } else if (mapItemType === 'Circle') { attributes.push({key: 'circle', value: additionalParams.coordinates}); diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md index 131a674099..b192042d2f 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md @@ -26,8 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
  • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
    • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
    • -
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • -
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • +
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    Note: The coordinates will be automatically converted according to the selected map type.
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md index be95f136ad..9af015d033 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute color of the trip path point. +A JavaScript function used to compute color of the path point. **Parameters:** @@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point. **Returns:** -Should return string value presenting color of the trip path point. +Should return string value presenting color of the path point. In case no data is returned, color value from **Color** settings field will be used. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md index 4ce996626a..e9e202ef85 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip. +A JavaScript function used to compute text or HTML code to be displayed in the path point tooltip. **Parameters:** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md new file mode 100644 index 0000000000..b2eafef8cb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md @@ -0,0 +1,22 @@ +#### Polyline label function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the polyline label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the polyline label. + +
    + +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md new file mode 100644 index 0000000000..6b49763393 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Polyline stroke color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the polyline. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting stroke color of the polyline. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md new file mode 100644 index 0000000000..0de9b8ee7a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Polyline tooltip function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the polyline tooltip. + +
    + +{% include widget/lib/map/tooltip_fn_examples %} 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 c32bf6801f..0c501f5c5e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1055,7 +1055,10 @@ "type": { "simple": "Simple", "script": "Script", - "geofencing" : "Geofencing" + "geofencing" : "Geofencing", + "propagation": "Propagation", + "related-entities-aggregation": "Related entities aggregation", + "related-entities-aggregation-hint": "Aggregation of data from related entities" }, "arguments": "Arguments", "decimals-by-default": "Decimals by default", @@ -1065,6 +1068,7 @@ "datasource": "Datasource", "add-argument": "Add argument", "test-script-function": "Test script function", + "test-expression-function": "Test expression function", "no-arguments": "No arguments configured", "argument-settings": "Argument settings", "argument-current": "Current entity", @@ -1073,6 +1077,7 @@ "argument-asset": "Asset", "argument-customer": "Customer", "argument-tenant": "Current tenant", + "argument-owner": "Current owner", "argument-relation-query": "Related entities", "argument-type": "Argument type", "see-debug-events": "See debug events", @@ -1088,6 +1093,7 @@ "shared-attributes": "Shared attributes", "attribute-key": "Attribute key", "default-value": "Default value", + "default-value-required": "Default value is required.", "limit": "Max values", "time-window": "Time window", "customer-name": "Customer name", @@ -1140,14 +1146,57 @@ "level": "Level", "direction-level": "Direction", "direction-up": "Up", + "direction-up-parent": "Up to parent", "direction-down": "Down", + "direction-down-child": "Down to child", "add-level": "Add level", "delete-level": "Delete level", "no-level": "No level configured", "levels-required": "At least one level must be configured.", "max-allowed-levels-error": "Relation level exceeds the maximum allowed.", + "propagation-path-related-entities": "Propagation path to related entities", + "propagate-type": { + "arguments-only": "Arguments only", + "expression-result": "Expression result" + }, + "data-propagate": "Data to propagate", + "output-key": "Output key", + "copy-output-key": "Copy output key", + "aggregation-path-related-entities": "Aggregation path to related entities", + "deduplication-interval": "Deduplication interval", + "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} second.", + "deduplication-interval-required": "Deduplication interval is required.", + "metrics": { + "metrics": "Metrics", + "metrics-empty": "At least one metric must be configured.", + "metric-name": "Metric name", + "copy-metric-name": "Copy metric name", + "aggregation": "Aggregation", + "aggregation-type": { + "avg": "Average", + "min": "Minimum", + "max": "Maximum", + "sum": "Sum", + "count": "Count", + "count-unique": "Count unique" + }, + "filtered": "Filtered", + "value-source": "Value source", + "value-source-type": { + "key": "Key", + "function": "Function" + }, + "no-metrics-configured": "No metrics configured", + "add-metric": "Add metric", + "max-metrics": "Maximum number of metrics reached.", + "metric-settings": "Metric settings", + "filter": "Filter", + "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." + }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", + "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.", "arguments-empty": "Arguments should not be empty.", "expression-required": "Expression is required.", "expression-invalid": "Expression is invalid", @@ -1157,7 +1206,13 @@ "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-forbidden": "Argument name is reserved and cannot be used.", - "name-required": "Mame is required.", + "output-key-required": "Output key is required.", + "output-key-pattern": "Output key is invalid.", + "output-key-duplicate": "Key with such name already exists.", + "output-key-max-length": "Output key should be less than 256 characters.", + "output-key-forbidden": "Output key is reserved and cannot be used.", + "entity-type-required": "Entity type is required", + "name-required": "Name is required.", "name-pattern": "Name is invalid.", "name-duplicate": "Name with such name already exists.", "name-max-length": "Name should be less than 256 characters.", @@ -1182,9 +1237,147 @@ "max-geofencing-zone": "Maximum number of geofencing zones reached.", "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", - "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second." + "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second.", + "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.", + "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", + "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", + "arguments-aggregation": "Defines input parameters used for filtering and aggregation.", + "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", + "metrics": "Defines metrics aggregated based on the configured arguments." } }, + "alarm-rule": { + "alarm-rules-tab": "Alarm rules", + "alarm-rule": "Alarm rule", + "alarm-rules": "Alarm rules", + "alarm-rules-old": "Old", + "alarm-rules-actual": "Actual", + "severities": "Severities", + "cleared": "Clear condition", + "delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", + "delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.", + "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?", + "delete-multiple-text": "Be careful, after the confirmation all selected alarm rules will be removed and all related data will become unrecoverable.", + "create": "Create new alarm rule", + "no-found": "No alarm rules found", + "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }", + "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", + "import": "Import alarm rule", + "export": "Export alarm rule", + "export-failed-error": "Unable to export alarm rule: {{error}}", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern": "Alarm type is invalid.", + "alarm-type-max-length": "Alarm type should be less than 256 characters.", + "clear-alarm": "Clear alarm", + "value-argument": "Argument", + "value-argument-required": "Argument is required.", + "static-settings": "Static settings", + "configuration": "Configuration", + "static-schedule": "Static", + "dynamic-schedule": "Dynamic", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "During {{during}}", + "condition-during-dynamic": "During \"{{ attribute }}\"", + "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", + "condition-repeat-times-dynamic": "Repeats \"{ attribute }\"", + "filter-preview": "Filter preview", + "condition-settings": "Condition settings", + "static": "Static", + "dynamic": "Dynamic", + "argument-filters": "Argument filters", + "argument-name": "Argument name", + "value-type": "Value type", + "general": "General", + "filter": "Filter", + "operation": "Operation", + "value-source": "Value source", + "value": "Value", + "ignore-case": "Ignore case", + "condition": "Condition", + "script": "Script", + "add-filter": "Add filter", + "edit-filter": "Edit filter", + "conditions": { + "simple": "Simple", + "duration": "Duration", + "repeating": "Repeating" + }, + "schedule-title": "Schedule", + "edit-schedule": "Edit alarm schedule", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": { + "any-time": "Active all the time", + "specific-time": "Active at a specific time", + "custom": "Custom" + }, + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To", + "schedule-days-of-week-required": "At least one day of week should be selected.", + "expression-type": { + "simple": "Simple", + "tbel": "TBEL" + }, + "operation-type": { + "and": "And", + "or": "Or" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numeric", + "boolean": "Boolean", + "complex": "Complex" + }, + "alarm-rule-additional-info": "Additional info", + "edit-alarm-rule-additional-info": "Edit additional info", + "alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info", + "alarm-rule-additional-info-hint": "Hint: use ${Argument name} to substitute values of the arguments that are used in alarm rule condition.", + "alarm-rule-mobile-dashboard": "Mobile dashboard", + "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", + "alarm-rule-no-mobile-dashboard": "No dashboard selected", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", + "edit-alarm-rule-condition": "Edit alarm rule condition", + "condition-type": "Condition type", + "select-alarm-severity": "Select alarm severity", + "add-create-alarm-rule-prompt": "Please add create alarm rule", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", + "condition-duration": "Condition duration", + "condition-duration-value": "Duration value", + "condition-duration-time-unit": "Time unit", + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", + "condition-duration-value-pattern": "Duration value should be integers.", + "condition-duration-value-required": "Duration value is required.", + "condition-duration-time-unit-required": "Time unit is required.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", + "condition-repeating-value-pattern": "Count of events should be integers.", + "condition-repeating-value-required": "Count of events is required.", + "create-alarm-rules": "Create alarm rules", + "clear-alarm-rule": "Clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "advanced-settings": "Advanced settings", + "propagate-alarm": "Propagate alarm to related entities", + "alarm-rule-relation-types-list": "Relation types", + "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", + "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", + "propagate-alarm-to-tenant": "Propagate alarm to Tenant", + "debugging": "Alarm rule debugging" + }, "ai-models": { "ai-models": "AI models", "ai-model": "AI model", @@ -2770,6 +2963,7 @@ "details": "Entity details", "no-entities-prompt": "No entities found", "no-data": "No data to display", + "show-all-columns":"Show All", "columns-to-display": "Columns to Display", "type-api-usage-state": "API Usage State", "type-edge": "Edge", @@ -2933,7 +3127,7 @@ "target-entity": "Target entity", "attributes-propagation": "Attributes propagation", "attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.", - "timeseries-data": "Times eries data", + "timeseries-data": "Time series data", "timeseries-data-hint": "Configure time series data keys of the target entity that will be accessible to the entity view. This time series data is read-only.", "search": "Search entity views", "selected-entity-views": "{ count, plural, =1 {1 entity view} other {# entity views} } selected", @@ -5564,7 +5758,7 @@ "time-series": "Time series", "latest": "Latest values", "web-sockets": "WebSockets", - "calculated-fields": "Calculated fields" + "calculated-fields-and-alarm-rules": "Calculated fields and alarm rules" }, "save-attribute": { "processing-settings": "Processing settings", @@ -5902,6 +6096,9 @@ "max-related-level-per-argument-required": "Relation level per 'Related entities' argument max number is required", "min-allowed-scheduled-update-interval": "Min allowed update interval for 'Related entities' arguments (seconds)", "min-allowed-scheduled-update-interval-range": "Min allowed update interval min number can't be negative", + "min-allowed-deduplication-interval": "Min allowed deduplication interval (seconds)", + "min-allowed-deduplication-interval-range": "Min allowed deduplication interval value can't be negative", + "min-allowed-deduplication-interval-required": "Min allowed deduplication interval is required", "min-allowed-scheduled-update-interval-required": "Min allowed update interval min number is required", "max-state-size": "State maximum size in KB", "max-state-size-range": "State maximum size in KB can't be negative", @@ -5978,6 +6175,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", "ws-limit-updates-per-session": "WS updates per session", + "relation-search-entity-limit": "Relation search entity limit", + "relation-search-entity-limit-hint": "Limits the number of entities resolved at the last level of the relation path. Applies to 'Related entities' arguments and Propagation fields.", + "relation-search-entity-limit-required": "Relation search entity limit", + "relation-search-entity-limit-range": "Relation search entity limit can't be less than '1'", "rate-limits": { "add-limit": "Add limit", "and-also-less-than": "and also less than", @@ -7069,7 +7270,8 @@ "marker": "Marker", "polygon": "Polygon", "rectangle": "Rectangle", - "circle": "Circle" + "circle": "Circle", + "polyline": "Polyline" }, "place-map-item": "Place map item", "map-item-tooltip": { @@ -7081,7 +7283,9 @@ "continue-draw-polygon": "Continue draw polygon", "finish-draw-polygon": "Finish draw polygon", "start-draw-circle": "Start draw circle", - "finish-draw-circle": "Finish draw circle" + "finish-draw-circle": "Finish draw circle", + "start-draw-polyline": "Start draw polyline", + "finish-draw-polyline": "Finish draw polyline" } }, "widgets-bundle": { @@ -8097,14 +8301,14 @@ "attribute-scope-server": "Server attribute", "attribute-scope-shared": "Shared attribute", "value-required": "Value required", - "image-settings": "Image settings", + "image-settings": "Image output settings", "image-format": "Image format", "image-format-jpeg": "JPEG", "image-format-png": "PNG", "image-format-webp": "WEBP", - "image-quality": "Image quality that use lossy compression such as jpeg and webp", - "max-image-width": "Maximum image width", - "max-image-height": "Maximum image height", + "image-quality": "Image quality", + "max-image-width": "Max width", + "max-image-height": "Max height", "action-buttons": "Action buttons", "show-action-buttons": "Show action buttons", "update-all-values": "Update all values, not only modified", @@ -8185,7 +8389,10 @@ "add-radio-option": "Add radio option", "radio-label-position": "Label position", "radio-label-position-before": "Before", - "radio-label-position-after": "After" + "radio-label-position-after": "After", + "save-image": "Save image", + "save-to-gallery": "Automatically store captured images in Image Gallery", + "public-image": "Makes image avaliable for any unauthorized user" }, "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", "qr-code": { @@ -8480,7 +8687,8 @@ "trips": "Trips", "markers": "Markers", "polygons": "Polygons", - "circles": "Circles" + "circles": "Circles", + "polylines": "Polylines" }, "data-layer": { "source": "Source", @@ -8677,6 +8885,25 @@ "finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle", "finish-circle-hint": "Circle: click to finish drawing" }, + "polyline": { + "polyline-key": "Polyline key", + "polyline-key-required": "Polyline key required", + "no-polylines": "No polylines configured", + "add-polylines": "Add polyline", + "polyline-configuration": "Polyline configuration", + "remove-polyline": "Remove polyline", + "edit": "Edit polyline", + "cut": "Cut polyline area", + "rotate": "Rotate polyline", + "remove-polyline-for": "Remove polyline for '{{entityName}}'", + "draw-polyline": "Draw polyline", + "polyline-place-first-point-hint-with-entity": "Polyline for '{{entityName}}': click to place first point", + "polyline-place-first-point-hint": "Polyline: click to place first point", + "finish-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to finish drawing", + "finish-polyline-hint": "Polyline: click to finish drawing", + "polyline-place-first-point-cut-hint": "Click to place first point", + "finish-polyline-cut-hint": "Click first marker to finish and save" + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" }, @@ -9180,7 +9407,11 @@ "alarm-column-error": "At least one alarm column should be specified", "table-tabs": "Table tabs", "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode", - "disable-sorting": "Disable sorting" + "disable-sorting": "Disable sorting", + "sort-by": "Sort tabs by", + "sort-asc": "Name Ascending", + "sort-desc": "Name Descending", + "sort-timestamp-option": "Created time" }, "latest-chart": { "total": "Total", @@ -9741,6 +9972,7 @@ "items-per-page-separator": "of" }, "language": { + "auto": "Auto", "language": "Language", "locales": { "ar_AE": "العربية (الإمارات العربية المتحدة)", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index 935ae65eb4..c3ddb5af74 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -1,7708 +1,9524 @@ -{ - "access": { - "unauthorized": "Neautorizuotas", - "unauthorized-access": "Neautorizuota prieiga", - "unauthorized-access-text": "Prieiga prie šio funkcionalumo leidžiama tik prisijungusiam vartotojui!", - "access-forbidden": "Prieiga uždrausta", - "access-forbidden-text": "Neturite teisių prieigai!
    Bandykite prisijungti kitu vartotoju.", - "refresh-token-expired": "Sesijos galiojimas baigėsi", - "refresh-token-failed": "Sesijos atnaujinti nepavyksta", - "permission-denied": "Leidimas nesuteiktas", - "permission-denied-text": "Šio veiksmo atlikti negalite!" - }, - "account": { - "account": "Account", - "notification-settings": "Notification settings" - }, - "action": { - "activate": "Aktyvuoti", - "suspend": "Sustabdyti", - "save": "Išsaugoti", - "saveAs": "Išsaugoti kaip", - "move": "Perkelti", - "cancel": "Atmesti", - "ok": "OK", - "delete": "Panaikinti", - "add": "Pridėti", - "yes": "Taip", - "no": "Ne", - "update": "Atnaujinti", - "remove": "Panaikinti", - "search": "Paieška", - "clear-search": "Išvalyti", - "assign": "Priskirti", - "unassign": "Atsieti", - "share": "Pasidalinti", - "make-private": "Riboti matomumą", - "make-public": "Neriboti matomumo", - "apply": "Taikyti", - "apply-changes": "Pritaikyti pakeitimus", - "edit-mode": "Redagavimo režimas", - "enter-edit-mode": "Redagavimo režimas", - "decline-changes": "Atšaukti pakeitimus", - "open": "Atverti", - "decline": "Atšaukti", - "close": "Uždaryti", - "back": "Atgal", - "run": "Paleisti", - "sign-in": "Prisijunkite!", - "edit": "Redaguoti", - "view": "Peržiūrėti", - "create": "Kurti", - "drag": "Vilkti", - "refresh": "Atnaujinti", - "undo": "Atšaukti", - "copy": "Kopijuoti", - "paste": "Įklijuoti", - "copy-reference": "Kopijuoti nuorodą", - "paste-reference": "Įklijuoti nuorodą", - "import": "Importuoti", - "export": "Eksportuoti", - "share-via": "Pasidalinti per {{provider}}", - "select": "Pasirinkti", - "continue": "Tęsti", - "discard-changes": "Atsisakyti pakeitimų", - "download": "Parsisiųsti", - "next": "Sekantis", - "next-with-label": "Sekantis: {{label}}", - "read-more": "Skaityti daugiau", - "hide": "Paslėpti", - "done": "Atlikta", - "print": "Spausdinti", - "restore": "Atkurti", - "confirm": "Patvirtinti", - "more": "Daugiau", - "less": "Mažiau", - "skip": "Praleisti", - "send": "Siųsti", - "reset": "Nustatyti iš naujo", - "show-more": "Rodyti daugiau", - "dont-show-again": "Daugiau neberodyti", - "see-documentation": "Žiūrėti dokumentacijoje", - "clear": "Išvalyti" - }, - "aggregation": { - "aggregation": "Agregavimas", - "function": "Duomenų aggregavimo funkcija", - "limit": "Maksimali reikšmė", - "group-interval": "Grupavimo intervalas", - "min": "Min", - "max": "Max", - "avg": "Vidurkis", - "sum": "Sum", - "count": "Kiekis", - "none": "Nėra" - }, - "admin": { - "settings": "Settings", - "general": "General", - "general-settings": "General Settings", - "home-settings": "Home Settings", - "home": "Home", - "outgoing-mail": "Mail Server", - "outgoing-mail-settings": "Outgoing Mail Server Settings", - "system-settings": "System Settings", - "test-mail-sent": "Test mail was successfully sent!", - "base-url": "Base URL", - "base-url-required": "Base URL is required.", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", - "mail-from": "Mail From", - "mail-from-required": "Mail From is required.", - "smtp-protocol": "SMTP protocol", - "smtp-host": "SMTP host", - "smtp-host-required": "SMTP host is required.", - "smtp-port": "SMTP port", - "smtp-port-required": "You must supply a smtp port.", - "smtp-port-invalid": "That doesn't look like a valid smtp port.", - "timeout-msec": "Timeout (msec)", - "timeout-required": "Timeout is required.", - "timeout-invalid": "That doesn't look like a valid timeout.", - "enable-tls": "Enable TLS", - "tls-version": "TLS version", - "enable-proxy": "Enable proxy", - "proxy-host": "Proxy host", - "proxy-host-required": "Proxy host is required.", - "proxy-port": "Proxy port", - "proxy-port-required": "Proxy port is required.", - "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", - "proxy-user": "Proxy user", - "proxy-password": "Proxy password", - "change-password": "Change password", - "send-test-mail": "Send test mail", - "use-system-mail-settings": "Use System Mail Server Settings", - "mail-templates": "Mail Templates", - "mail-template-settings": "Mail Templates Settings", - "use-system-mail-template-settings": "Use System Mail Templates", - "mail-template": { - "mail-template": "Mail template", - "test": "Test email message", - "activation": "Account activation message", - "account-activated": "Account activated message", - "account-lockout": "Account lockout message", - "reset-password": "Reset password message", - "password-was-reset": "Password was reset message", - "user-activated": "User activated message", - "user-registered": "User registered message", - "api-usage-state-enabled": "Api usage state enabled", - "api-usage-state-warning": "Api usage state warning", - "api-usage-state-disabled": "Api usage state disabled", - "two-fa-verification": "2FA verification message" - }, - "mail-subject": "Mail Subject", - "mail-body": "Mail body", - "sms-provider": "SMS provider", - "sms-provider-settings": "SMS provider settings", - "use-system-sms-settings": "Use System SMS provider settings", - "sms-provider-type": "SMS provider type", - "sms-provider-type-required": "SMS provider type is required.", - "sms-provider-type-aws-sns": "Amazon SNS", - "sms-provider-type-twilio": "Twilio", - "sms-provider-type-smpp": "SMPP", - "aws-access-key-id": "AWS Access Key ID", - "aws-access-key-id-required": "AWS Access Key ID is required", - "aws-secret-access-key": "AWS Secret Access Key", - "aws-secret-access-key-required": "AWS Secret Access Key is required", - "aws-region": "AWS Region", - "aws-region-required": "AWS Region is required", - "number-from": "Phone Number From", - "number-from-required": "Phone Number From is required.", - "number-to": "Phone Number To", - "number-to-required": "Phone Number To is required.", - "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", - "phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX", - "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", - "phone-number-pattern-twilio": "Invalid phone number. Should be in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX.", - "sms-message": "SMS message", - "sms-message-required": "SMS message is required.", - "sms-message-max-length": "SMS message can't be longer 1600 characters", - "twilio-account-sid": "Twilio Account SID", - "twilio-account-sid-required": "Twilio Account SID is required", - "twilio-account-token": "Twilio Account Token", - "twilio-account-token-required": "Twilio Account Token is required", - "send-test-sms": "Send test SMS", - "test-sms-sent": "Test SMS was successfully sent!", - "security-settings": "Security settings", - "password-policy": "Password policy", - "minimum-password-length": "Minimum password length", - "minimum-password-length-required": "Minimum password length is required", - "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50", - "minimum-uppercase-letters": "Minimum number of uppercase letters", - "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative", - "minimum-lowercase-letters": "Minimum number of lowercase letters", - "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative", - "minimum-digits": "Minimum number of digits", - "minimum-digits-range": "Minimum number of digits can't be negative", - "minimum-special-characters": "Minimum number of special characters", - "minimum-special-characters-range": "Minimum number of special characters can't be negative", - "password-expiration-period-days": "Password expiration period in days", - "password-expiration-period-days-range": "Password expiration period in days can't be negative", - "password-reuse-frequency-days": "Password reuse frequency in days", - "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative", - "allow-whitespace": "Allow whitespace", - "general-policy": "General policy", - "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", - "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", - "user-lockout-notification-email": "In case user account lockout, send notification to email", - "domain-name": "Domain name", - "domain-name-unique": "Domain name and protocol need to unique.", - "domain-name-max-length": "Domain name should be less than 256", - "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", - "connection-settings": "Connection settings", - "oauth2": { - "access-token-uri": "Access token URI", - "access-token-uri-required": "Access token URI is required.", - "activate-user": "Activate user", - "add-domain": "Add domain", - "delete-domain": "Delete domain", - "add-provider": "Add provider", - "delete-provider": "Delete provider", - "allow-user-creation": "Allow user creation", - "always-fullscreen": "Always fullscreen", - "authorization-uri": "Authorization URI", - "authorization-uri-required": "Authorization URI is required.", - "client-authentication-method": "Client authentication method", - "client-id": "Client ID", - "client-id-required": "Client ID is required.", - "client-id-max-length": "Client ID should be less than 256", - "client-secret": "Client secret", - "client-secret-required": "Client secret is required.", - "client-secret-max-length": "Client secret should be less than 2049", - "custom-setting": "Custom settings", - "customer-name-pattern": "Customer name pattern", - "customer-name-pattern-max-length": "Customer name pattern should be less than 256", - "parent-customer-name-pattern": "Parent customer name pattern", - "user-groups-name-pattern": "User groups name pattern", - "default-dashboard-name": "Default dashboard name", - "default-dashboard-name-max-length": "Default dashboard name should be less than 256", - "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", - "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", - "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", - "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", - "email-attribute-key": "Email attribute key", - "email-attribute-key-required": "Email attribute key is required.", - "email-attribute-key-max-length": "Email attribute key should be less than 32", - "first-name-attribute-key": "First name attribute key", - "first-name-attribute-key-max-length": "First name attribute key should be less than 32", - "general": "General", - "jwk-set-uri": "JSON Web Key URI", - "last-name-attribute-key": "Last name attribute key", - "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", - "login-button-icon": "Login button icon", - "login-button-label": "Provider label", - "login-button-label-placeholder": "Login with $(Provider label)", - "login-button-label-required": "Label is required.", - "login-provider": "Login provider", - "mapper": "Mapper", - "new-domain": "New domain", - "oauth2": "OAuth2", - "password-max-length": "Password should be less than 256", - "redirect-uri-template": "Redirect URI template", - "copy-redirect-uri": "Copy redirect URI", - "registration-id": "Registration ID", - "registration-id-required": "Registration ID is required.", - "registration-id-unique": "Registration ID need to unique for the system.", - "scope": "Scope", - "scope-required": "Scope is required.", - "tenant-name-pattern": "Tenant name pattern", - "tenant-name-pattern-required": "Tenant name pattern is required.", - "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", - "tenant-name-strategy": "Tenant name strategy", - "type": "Mapper type", - "uri-pattern-error": "Invalid URI format.", - "url": "URL", - "url-pattern": "Invalid URL format.", - "url-required": "URL is required.", - "url-max-length": "URL should be less than 256", - "user-info-uri": "User info URI", - "user-info-uri-required": "User info URI is required.", - "username-max-length": "User name should be less than 256", - "user-name-attribute-name": "User name attribute key", - "user-name-attribute-name-required": "User name attribute key is required", - "protocol": "Protocol", - "domain-schema-http": "HTTP", - "domain-schema-https": "HTTPS", - "domain-schema-mixed": "HTTP+HTTPS", - "enable": "Enable OAuth2 settings", - "domains": "Domains", - "mobile-apps": "Mobile applications", - "no-mobile-apps": "No applications configured", - "mobile-package": "Application package", - "mobile-package-placeholder": "Ex.: my.example.app", - "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", - "mobile-package-unique": "Application package must be unique.", - "mobile-app-secret": "Application secret", - "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", - "copy-mobile-app-secret": "Copy application secret", - "add-mobile-app": "Add application", - "delete-mobile-app": "Delete application info", - "providers": "Providers", - "platform-web": "Web", - "platform-android": "Android", - "platform-ios": "iOS", - "all-platforms": "All platforms", - "smtp-provider": "SMTP provider", - "allowed-platforms": "Allowed platforms", - "authentication": "Authentication", - "basic": "Basic", - "provider": "Provider", - "redirect-url": "Redirect URI", - "domain-name": "Domain name", - "redirect-url-template": "Redirect URI template", - "microsoft-tenant-id": "Directory (tenant) Id", - "microsoft-tenant-id-required": "Directory (tenant) Id is required", - "token-uri": "Token URI", - "token-uri-required": "Token URI is required", - "redirect-uri": "Redirect URI", - "google-provider": "Google", - "microsoft-provider": "Office 365", - "sendgrid-provider": "Sendgrid", - "custom-provider": "Custom", - "generate-access-token": "Generate access token", - "update-access-token": "Update access token", - "access-token-status": "Access token status:", - "token-status-generated": "generated", - "token-status-not-generated": "not generated" - }, - "smpp-provider": { - "smpp-version": "SMPP version", - "smpp-host": "SMPP host", - "smpp-host-required": "SMPP host is required", - "smpp-port": "SMPP port", - "smpp-port-required": "SMPP port is required", - "system-id": "System ID", - "system-id-required": "System ID is required", - "password": "Password", - "password-required": "Password is required", - "type-settings": "Type settings", - "source-settings": "Source settings", - "destination-settings": "Destination settings", - "additional-settings": "Additional settings", - "system-type": "System type", - "bind-type": "Bind type", - "service-type": "Service type", - "source-address": "Source address", - "source-ton": "Source TON", - "source-npi": "Source NPI", - "destination-ton": "Destination TON (Type of Number)", - "destination-npi": "Destination NPI (Numbering Plan Identification)", - "address-range": "Address range", - "coding-scheme": "Coding scheme", - "bind-type-tx": "Transmitter", - "bind-type-rx": "Receiver", - "bind-type-trx": "Transciever", - "ton-unknown": "Unknown", - "ton-international": "International", - "ton-national": "National", - "ton-network-specific": "Network Specific", - "ton-subscriber-number": "Subscriber Number", - "ton-alphanumeric": "Alphanumeric", - "ton-abbreviated": "Abbreviated", - "npi-unknown": "0 - Unknown", - "npi-isdn": "1 - ISDN/telephone numbering plan (E163/E164)", - "npi-data-numbering-plan": "3 - Data numbering plan (X.121)", - "npi-telex-numbering-plan": "4 - Telex numbering plan (F.69)", - "npi-land-mobile": "6 - Land Mobile (E.212)", - "npi-national-numbering-plan": "8 - National numbering plan", - "npi-private-numbering-plan": "9 - Private numbering plan", - "npi-ermes-numbering-plan": "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)", - "npi-internet": "13 - Internet (IP)", - "npi-wap-client-id": "18 - WAP Client Id (to be defined by WAP Forum)", - "scheme-smsc": "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free)", - "scheme-ia5": "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))", - "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)", - "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", - "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)", - "scheme-jis": "5 - JIS (X 0208-1990)", - "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", - "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", - "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", - "scheme-pictogram-encoding": "9 - Pictogram Encoding", - "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)", - "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)", - "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)" - }, - "queue-select-name": "Select queue name", - "queue-name": "Name", - "queue-name-required": "Queue name is required!", - "queues": "Queues", - "queue-partitions": "Partitions", - "queue-submit-strategy": "Submit strategy", - "queue-processing-strategy": "Processing strategy", - "queue-configuration": "Queue configuration", - "repository-settings": "Repository settings", - "repository": "Repository", - "repository-url": "Repository URL", - "repository-url-required": "Repository URL is required.", - "default-branch": "Default branch name", - "repository-read-only": "Read-only", - "show-merge-commits": "Show merge commits", - "authentication-settings": "Authentication settings", - "auth-method": "Authentication method", - "auth-method-username-password": "Password / access token", - "auth-method-username-password-hint": "GitHub users must use access tokens with write permissions to the repository.", - "auth-method-private-key": "Private key", - "password-access-token": "Password / access token", - "change-password-access-token": "Change password / access token", - "private-key": "Private key", - "drop-private-key-file-or": "Drag and drop a private key file or", - "passphrase": "Passphrase", - "enter-passphrase": "Enter passphrase", - "change-passphrase": "Change passphrase", - "check-access": "Check access", - "check-repository-access-success": "Repository access successfully verified!", - "delete-repository-settings-title": "Are you sure you want to delete repository settings?", - "delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.", - "auto-commit-settings": "Auto-commit settings", - "auto-commit": "Auto-commit", - "auto-commit-entities": "Auto-commit entities", - "no-auto-commit-entities-prompt": "No entities configured for auto-commit", - "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", - "delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.", - "2fa": { - "2fa": "Two-factor authentication", - "available-providers": "Available providers", - "issuer-name": "Issuer name", - "issuer-name-required": "Issuer name is required.", - "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", - "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", - "number-of-checking-attempts": "Number of checking attempts", - "number-of-checking-attempts-pattern": "Number of checking attempts must be a positive integer.", - "number-of-checking-attempts-required": "Number of checking attempts is required.", - "number-of-codes": "Number of codes", - "number-of-codes-pattern": "Number of codes must be a positive integer.", - "number-of-codes-required": "Number of codes is required.", - "provider": "Provider", - "retry-verification-code-period": "Retry verification code period (sec)", - "retry-verification-code-period-pattern": "Minimal period time is 5 sec", - "retry-verification-code-period-required": "Retry verification code period is required.", - "total-allowed-time-for-verification": "Total allowed time for verification (sec)", - "total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec", - "total-allowed-time-for-verification-required": "Total allowed time is required.", - "use-system-two-factor-auth-settings": "Use system two factor auth settings", - "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-lifetime": "Verification code lifetime (sec)", - "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", - "verification-code-lifetime-required": "Verification code lifetime is required.", - "verification-message-template": "Verification message template", - "verification-limitations": "Verification limitations", - "verification-message-template-pattern": "Verification message need to contains pattern: ${code}", - "verification-message-template-required": "Verification message template is required.", - "within-time": "Within time (sec)", - "within-time-pattern": "Time must be a positive integer.", - "within-time-required": "Time is required." - }, - "jwt": { - "security-settings": "JWT security settings", - "issuer-name": "Issuer name", - "issuer-name-required": "Issuer name is required.", - "signings-key": "Signing key", - "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", - "signings-key-required": "Signing key is required.", - "signings-key-min-length": "Signing key must be at least 256 bits of data.", - "signings-key-base64": "Signing key must be base64 format.", - "expiration-time": "Token expiration time (sec)", - "expiration-time-required": "Token expiration time is required.", - "expiration-time-min": "Minimum time is 60 seconds (1 minute).", - "refresh-expiration-time": "Refresh token expiration time (sec)", - "refresh-expiration-time-required": "Refresh token expiration time is required.", - "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", - "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", - "generate-key": "Generate key", - "info-header": "All users will be to re-logined", - "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets." - }, - "resources": "Resources", - "notifications": "Notifications", - "notifications-settings": "Notifications settings", - "slack-api-token": "Slack API token", - "slack": "Slack", - "slack-settings": "Slack settings" - }, - "alarm": { - "alarm": "Įspėjimas", - "alarms": "Įspėjimai", - "all-alarms": "Visi Įspėjimai", - "select-alarm": "Pasirinkti įspėjimą", - "no-alarms-matching": "Su '{{entity}}' susijusių įspėjimų nėra.", - "alarm-required": "Įspėjimas yra privalomas", - "alarm-filter": "Įspėjimų filtras", - "filter": "Filtras", - "alarm-status": "Įspėjimo statusas", - "alarm-status-list": "Įspėjimo statusų sąrašas", - "any-status": "Visi statusai", - "search-status": { - "ANY": "Visi", - "ACTIVE": "Aktyvūs", - "CLEARED": "Atmesti", - "ACK": "Pripažinti", - "UNACK": "Nepatvirtinti" - }, - "display-status": { - "ACTIVE_UNACK": "Aktyvūs nepatvirtinti", - "ACTIVE_ACK": "Aktyvūs pripažinti", - "CLEARED_UNACK": "Atmesti nepatvirtinti", - "CLEARED_ACK": "Atmesti pripažinti" - }, - "no-alarms-prompt": "Įspėjimų nėra", - "created-time": "Sukūrimo laikas", - "type": "Tipas", - "severity": "Lygis", - "originator": "Iniciatorius", - "originator-type": "Iniciatoriaus tipas", - "details": "Pastabos", - "originator-label": "Iniciatoriaus pavadinimas", - "assign": "Priskirti", - "assignments": "Priskyrimai", - "assignee": "Atsakingas", - "assignee-id": "Atsakingo asmens ID", - "assignee-first-name": "Atsakingo asmens vardas", - "assignee-last-name": "Atsakingo asmens pavardė", - "assignee-email": "Atsakingo asmens e-paštas", - "unassigned": "Panaikinti priskyrimą", - "assignee-not-set": "Visi", - "status": "Statusas", - "alarm-details": "Įspėjimo pastabos", - "start-time": "Pradžia", - "assign-time": "Priskyrimo laikas", - "end-time": "Pabaiga", - "ack-time": "Pripažinimo laikas", - "clear-time": "Atmetimo laikas", - "duration": "Trukmė", - "alarm-severity-list": "Įspėjimų lygiai", - "any-severity": "Visi lygiai", - "severity-critical": "Kritinis", - "severity-major": "Svarbus", - "severity-minor": "Antraeilis", - "severity-warning": "Įspėjimas", - "severity-indeterminate": "Nenustatytas", - "acknowledge": "Patvirtinti", - "clear": "Atmesti", - "delete": "Panaikinti", - "search": "Įspėjimo paieška", - "selected-alarms": "Pasirinkta { count, plural, =1 {1 įspėjimas} other {# įspėjimai} }", - "no-data": "Nėra duomenų", - "polling-interval": "Įspėjimų tikrinimo intervalas (sek)", - "polling-interval-required": "Reikalingas įspėjimų tikrinimo intervalas", - "min-polling-interval-message": "Mažiausias įspėjimų tikrinimo intervalas yra 1 sekundė.", - "aknowledge-alarms-title": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "aknowledge-alarms-text": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "aknowledge-alarm-title": "Pripažinti įspėjimą", - "aknowledge-alarm-text": "Pripažinti įspėjimą?", - "selected-alarms-are-acknowledged": "Pasirinkti įspėjimai pripažinti", - "clear-alarms-title": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "clear-alarms-text": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "clear-alarm-title": "Atmesti įspėjimą", - "clear-alarm-text": "Atmesti įspėjimą?", - "delete-alarms-title": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "delete-alarms-text": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "selected-alarms-are-cleared": "Pasirinkti įspėjimai atmesti", - "alarm-status-filter": "Įspėjimų statusai", - "alarm-filter-title": "Įspėjimų filtras", - "assigned": "Priskirta", - "filter-title": "Filtras", - "max-count-load": "Didžiausias rodomų įspėjimų skaičius (0 – neribotas)", - "max-count-load-required": "Reikalingas didžiausias rodomų įspėjimų skaičius (0 – neribotas).", - "max-count-load-error-min": "Mažiausia reikšmė yra 0.", - "fetch-size": "Fetch size", - "fetch-size-required": "Fetch size is required.", - "fetch-size-error-min": "Mažiausia rekšmė yra 10.", - "alarm-type-list": "Įspėjimų tipai", - "any-type": "Visi tipai", - "assigned-to-current-user": "Priskirta prisijungusiam vartotojui", - "assigned-to-me": "Priskirta man", - "search-propagated-alarms": "Ieškoti išplatintų įspėjimų", - "comments": "Įspėjimo pastabos", - "show-more": "Rodyti daugiau", - "additional-info": "Papildoma informacija", - "alarm-type": "Įspėjimo tipas", - "enter-alarm-type": "Įveskite įspėjimo tipą", - "no-alarm-types-matching": "Nėra įspėjimų tipų, kurie atitiktų '{{entitySubtype}}'.", - "alarm-type-list-empty": "Nepasirinktas įspėjimų tipas" - }, - "alarm-activity": { - "add": "Pridėti komentarą...", - "alarm-comment": "Įspėjimo komentaras", - "comments": "Komentarai", - "delete-alarm-comment": "Ištrinti komentarą?", - "refresh": "Atnaujinti", - "oldest-first": "Seniausi viršuje", - "newest-first": "Naujausi viršuje", - "activity": "Veikla", - "export": "Eksportuoti į CSV", - "author": "Sukūrė", - "created-date": "Sukūrimo dara", - "edited-date": "Redagavimo data", - "text": "Tekstas", - "system": "Sistema" - }, - "alias": { - "add": "Pridėti pseudonimą", - "edit": "Redaguoti pseudonimą", - "name": "Pseudonimas", - "name-required": "Reikalingas pseudonimas", - "duplicate-alias": "Toks pseudonimas sistemoje jau yra.", - "filter-type-single-entity": "Vienas subjektas", - "filter-type-entity-group": "Subjektų grupė", - "filter-type-entity-list": "Subjektų sąrašas", - "filter-type-entity-name": "Subjekto pavadinimas", - "filter-type-entity-type": "Subjekto tipas", - "filter-type-entity-group-list": "Subjektų grupių sąrašas", - "filter-type-entity-group-name": "Subjektų grupės pavadinimas", - "filter-type-entities-by-group-name": "Subjektai pagal grupės pavadinimą", - "filter-type-state-entity": "Subjektas iš skydelio būsenos", - "filter-type-state-entity-description": "Subjektas iš skydelio būsenos parametrų", - "filter-type-state-entity-owner": "Skydelio būsenos subjekto savininkas", - "filter-type-state-entity-owner-description": "Subjekto, iš skydelio būsenos parametrų, savininkas", - "filter-type-asset-type": "Turto tipas", - "filter-type-asset-type-description": "Turtas, kurio tipas '{{assetTypes}}'", - "filter-type-asset-type-and-name-description": "Turtas, kurio tipas '{{assetTypes}}' ir kurio pavadinimas prasideda '{{prefix}}'", - "filter-type-device-type": "Įrenginio tipas", - "filter-type-device-type-description": "Įrenginiai, kurių tipas '{{deviceTypes}}'", - "filter-type-device-type-and-name-description": "Įrenginiai, kurių tipas '{{deviceTypes}}' ir kurių pavadinimai '{{prefix}}'", - "filter-type-entity-view-type": "Subjekto rodinio tipas", - "filter-type-entity-view-type-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}' ir kurių pavadinimai prasideda '{{prefix}}'", - "filter-type-edge-type": "Edge type", - "filter-type-edge-type-description": "Edges of type '{{edgeTypes}}'", - "filter-type-edge-type-and-name-description": "Edges of type '{{edgeTypes}}' and with name starting with '{{prefix}}'", - "filter-type-relations-query": "Ryšių užklausa", - "filter-type-relations-query-description": "{{entities}} kurie turi {{relationType}} ryšį {{direction}} {{rootEntity}}", - "filter-type-edge-search-query": "Edge search query", - "filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-scheduler-event": "Scheduler events", - "filter-type-scheduler-event-type-description": "Scheduler events with type '{{eventType}}'", - "filter-type-scheduler-event-originator-description": "Scheduler events of specific originator", - "filter-type-scheduler-event-type-originator-description": "Scheduler events of specific originator with '{{eventType}}'", - "filter-type-asset-search-query": "Turto paieškos užklausa", - "filter-type-asset-search-query-description": "{{assetTypes}} tipo turtas, turintis {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Įrenginio paieškos užklausa", - "filter-type-device-search-query-description": "{{deviceTypes}} tipo įrenginiai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Subjektų rodinio paieškos užklausa", - "filter-type-entity-view-search-query-description": "{{entityViewTypes}} tipo subjektai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-apiUsageState": "Api Usage State", - "entity-filter": "Subjektų filtras", - "resolve-multiple": "Traktuoti kaip kelis subjektus", - "filter-type": "Filtro tipas", - "filter-type-required": "Filtro tipas būtinas.", - "entity-filter-no-entity-matched": "Subjektų, atitinkančių filtro kriterijus, nerasta.", - "no-entity-filter-specified": "Subjektų filtras nenustatytas", - "root-state-entity": "Subjektą, iš skydelio būsenos, naudoti kaip pagrindinį", - "group-state-entity": "Skydelio būsenos subjektą naudoti kaip subjektų grupę", - "group-state-entity-owner": "Skydelio būsenos subjektą naudoti kaip subjektų grupės sąvininką", - "last-level-relation": "Gauti tik paskutinio lygio ryšį", - "root-entity": "Pagridinis subjektas", - "state-entity-parameter-name": "Būsenos subjekto parametro pavadinimas", - "default-state-entity": "Numatytasis būsenos subjektas", - "default-state-entity-group": "Numatytoji būsenos subjektų grupė", - "default-entity-parameter-name": "Pagal nutylėjimą", - "max-relation-level": "Maksimalus ryšių lygis", - "unlimited-level": "Neribojamas lygis", - "state-entity": "Skydelio būsenos subjektas", - "entities-of-group-state-entity": "Subjektai iš skydelio būsenos subjektų grupės", - "all-entities": "Visi subjektai", - "any-relation": "visi", - "originator": "Iniciatorius", - "originator-state-entity": "Skydelio būsenos subjektą naudoti kaip iniciatorių", - "scheduler-event-type": "Scheduler event type" - }, - "asset": { - "all": "Visas", - "all-assets": "Visas turtas", - "groups": "Grupės", - "shared": "Bendrinamas", - "asset": "Turtas", - "assets": "Turtas", - "management": "Turto valdymas", - "view-assets": "Peržiūrėti turtą", - "add": "Pridėti turtą", - "asset-type-max-length": "Turto tipas turi būti trumpesnis nei 256 simboliai", - "assign-to-customer": "Priskirti klientui", - "assign-asset-to-customer": "Turtą priskirti klientui", - "assign-asset-to-customer-text": "Pasirinkite turtą, kurį norite priskiri klientui", - "no-assets-text": "Turto nerasta", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskiriamas turtas", - "public": "Viešas", - "assignedToCustomer": "Priskirti klientui", - "make-public": "Neriboti turto matomumo", - "make-private": "Riboti turto matomumą", - "unassign-from-customer": "Atsieti nuo kliento", - "delete": "Panaikinti turtą", - "asset-public": "Turtas yra viešas", - "asset-type": "Turto tipas", - "asset-type-required": "Turto tipas būtinas.", - "select-asset-type": "Pasirinkite turto tipą", - "enter-asset-type": "Įveskite turto tipą", - "any-asset": "Visas turtas", - "no-asset-types-matching": "Turto tipas, atitinkantis '{{entitySubtype}}', nerastas.", - "asset-type-list-empty": "Nepasirinkti turto tipai.", - "asset-types": "Turto tipai", - "name": "Pavadinimas", - "name-required": "Pavadinimas yra privalomas.", - "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", - "label-max-length": "Vardas turi būti trumpesnis nei 256 simboliai", - "description": "Aprašymas", - "type": "Tipas", - "type-required": "Tipas yra prvalomas.", - "details": "Pastabos", - "events": "Įvykiai", - "add-asset-text": "Pridėti turtą", - "asset-details": "Pastabos apie turtą", - "assign-assets": "Priskirti turtą", - "assign-assets-text": "Priskirti { count, plural, =1 {1 turtą} other {# turtus} } to customer", - "assign-asset-to-edge-title": "Assign Asset(s) To Edge", - "assign-asset-to-edge-text": "Please select the assets to assign to the edge", - "delete-assets": "Pašalinti turtą", - "unassign-assets": "Atsieti turtą", - "unassign-assets-action-title": "Atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo kliento?", - "assign-new-asset": "Prisirti naują turtą", - "delete-asset-title": "Pašalinti '{{assetName}}' turtą?", - "delete-asset-text": "Būkite dėmesingi, po patvirtinimo, turtas ir su juo susijusi informacija, bus pašalinta.", - "delete-assets-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }?", - "delete-assets-action-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }", - "delete-assets-text": "Būkite dėmesingi, po patvirtinimo, visi turto įrašai ir su jais susijusi informacija, bus pašalinta.", - "make-public-asset-title": "Ar tikrai norite neriboti turto '{{assetName}}' matomumo?", - "make-public-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija bus prieinama visiems.", - "make-private-asset-title": "Ar tikrai norite riboti turto '{{assetName}}' matomumą?", - "make-private-asset-text": "Po patvirtinimo, turtas ir visa su juo susijusi informacija nebus prieinama kitiems.", - "unassign-asset-title": "Ar tikrai norite atsieti turtą '{{assetName}}'?", - "unassign-asset-text": "Po patvirtinimo turtas bus atsietas ir klientas neteks prieigos prie jo.", - "unassign-asset": "Atsieti turtą", - "unassign-assets-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} }?", - "unassign-assets-text": "Po patvirtinimo visas turtas bus atsietas ir klientas neteks prieigos prie jo.", - "copyId": "Kopijuoti turto Id", - "idCopiedMessage": "Turto Id nukopijuotas į iškarpinę", - "select-asset": "Pasirinkti turtą", - "no-assets-matching": "Turtas atitinkantis '{{entity}}' nerastas.", - "asset-required": "Turtas būtinas", - "name-starts-with": "Turto pavadinimas prasideda", - "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%turto_pavadinimo_fragmentas%', '%turto_pavadinimo_pabaiga', 'turto_pavadinimo_pradžia%'.", - "search": "Turto paieška", - "select-group-to-add": "Pasirinkti grupę, į kurią turtą pridėti", - "select-group-to-move": "Pasirinkite grupę, į kurią turtą perkelti", - "remove-assets-from-group": "Ar tikrai norite { count, plural, =1 {1 turtą} other {# turtus} } pašalinti iš grupės '{{entityGroup}}'?", - "group": "Turto grupė", - "list-of-groups": "{ count, plural, =1 {Viena turto grupė} other { # Turto grupių sąrašas} }", - "group-name-starts-with": "Turto grupės, kurių pavadinimai prasideda '{{prefix}}'", - "import": "Importuoti turtą", - "asset-file": "Turto failas", - "label": "Etiketė", - "assign-asset-to-edge": "Assign Asset(s) To Edge", - "unassign-asset-from-edge": "Unassign asset", - "unassign-asset-from-edge-title": "Are you sure you want to unassign the asset '{{assetName}}'?", - "unassign-asset-from-edge-text": "After the confirmation the asset will be unassigned and won't be accessible by the edge.", - "unassign-assets-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 asset} other {# assets} }?", - "unassign-assets-from-edge-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the edge.", - "selected-assets": "Pasirinkta { count, plural, =1 {1 turtas} other {# turtai} }" - }, - "attribute": { - "attributes": "Atributai", - "latest-telemetry": "Paskutinė telemetrija", - "no-latest-telemetry": "Telemetrijos nėra", - "attributes-scope": "Subjekto atributų kontekstas", - "scope-telemetry": "Telemetrija", - "scope-latest-telemetry": "Paskutinė telemetrija", - "scope-client": "Kliento atributai", - "scope-server": "Serverio atributai", - "scope-shared": "Bendrinami atributai", - "add": "Pridėti atributą", - "add-attribute-prompt": "Pridėkite atributą", - "key": "Raktas", - "key-max-length": "Raktas negali viršyti 256 simbolių", - "last-update-time": "Atnaujinimo laikas", - "key-required": "Atributo raktas būtinas.", - "value": "Reikšmė", - "value-required": "Atributo reikšmė būtina.", - "telemetry-key-required": "Telemetrijos raktas būtinas", - "telemetry-value-required": "Telemetrijos reikšmė būtina", - "delete-attributes-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 atributą} other {# atributus} }?", - "delete-attributes-text": "Būkite dėmesingi, po patvirtinimo pasirinkti atributai bus pašalinti.", - "delete-attributes": "Pašalinti atributus", - "enter-attribute-value": "Įveskite atributo reikšmę", - "show-on-widget": "Rodyti valdiklyje", - "widget-mode": "Valdiklio režimas", - "next-widget": "Sekantis valdiklis", - "prev-widget": "Ankstesnis valdiklis", - "add-to-dashboard": "Pridėti į skydelį", - "add-widget-to-dashboard": "Pridėti valdiklį į skydelį", - "selected-attributes": "Pasirinkta { count, plural, =1 {1 atributas} other {# atributai} }", - "selected-telemetry": "Pasirinkta { count, plural, =1 {1 telemetrijos blokas} other {# telemetrijos blokai} }", - "no-attributes-text": "Atributų nėra", - "no-telemetry-text": "Telemetrijos nėra", - "copy-key": "Kopijuoti raktą", - "add-telemetry": "Pridėti telemetriją", - "copy-value": "Kopijuoti reikšmę", - "delete-timeseries": { - "start-time": "Pradžia", - "ends-on": "Pabaiga", - "strategy": "Strategija", - "delete-strategy": "Šalinimo startegija", - "all-data": "Pašalinti visus duomenis", - "all-data-except-latest-value": "Pašalinti visus duomenis išskyrus naujausią reikšmę", - "latest-value": "Pašalinti naujausią reikšmę", - "all-data-for-time-period": "Pašalinti visus duomenis periodui", - "rewrite-latest-value": "Perrašyti naujausią reikšmę" - } +{ + "access": { + "unauthorized": "Neautorizuotas", + "unauthorized-access": "Neautorizuota prieiga", + "unauthorized-access-text": "Prieiga prie šio funkcionalumo leidžiama tik prisijungusiam vartotojui!", + "access-forbidden": "Prieiga uždrausta", + "access-forbidden-text": "Neturite teisių prieigai!
    Bandykite prisijungti kitu vartotoju.", + "refresh-token-expired": "Sesijos galiojimas baigėsi", + "refresh-token-failed": "Sesijos atnaujinti nepavyksta", + "permission-denied": "Leidimas nesuteiktas", + "permission-denied-text": "Šio veiksmo atlikti negalite!" + }, + "account": { + "account": "Paskyra", + "notification-settings": "Pranešimų nustatymai" + }, + "action": { + "activate": "Aktyvuoti", + "suspend": "Sustabdyti", + "save": "Išsaugoti", + "saveAs": "Išsaugoti kaip", + "move": "Perkelti", + "cancel": "Atmesti", + "ok": "OK", + "delete": "Panaikinti", + "add": "Pridėti", + "yes": "Taip", + "no": "Ne", + "update": "Atnaujinti", + "remove": "Panaikinti", + "search": "Paieška", + "clear-search": "Išvalyti", + "assign": "Priskirti", + "unassign": "Atsieti", + "share": "Pasidalinti", + "make-private": "Riboti matomumą", + "apply": "Taikyti", + "apply-changes": "Pritaikyti pakeitimus", + "edit-mode": "Redagavimo režimas", + "enter-edit-mode": "Redagavimo režimas", + "decline-changes": "Atšaukti pakeitimus", + "decline": "Atšaukti", + "close": "Uždaryti", + "back": "Atgal", + "run": "Paleisti", + "sign-in": "Prisijunkite!", + "edit": "Redaguoti", + "view": "Peržiūrėti", + "create": "Kurti", + "drag": "Vilkti", + "refresh": "Atnaujinti", + "undo": "Atšaukti", + "copy": "Kopijuoti", + "paste": "Įklijuoti", + "copy-reference": "Kopijuoti nuorodą", + "paste-reference": "Įklijuoti nuorodą", + "import": "Importuoti", + "export": "Eksportuoti", + "share-via": "Pasidalinti per {{provider}}", + "select": "Pasirinkti", + "continue": "Tęsti", + "discard-changes": "Atsisakyti pakeitimų", + "download": "Parsisiųsti", + "next": "Sekantis", + "next-with-label": "Sekantis: {{label}}", + "read-more": "Skaityti daugiau", + "hide": "Paslėpti", + "test": "Test", + "done": "Atlikta", + "print": "Spausdinti", + "restore": "Atkurti", + "confirm": "Patvirtinti", + "more": "Daugiau", + "less": "Mažiau", + "skip": "Praleisti", + "send": "Siųsti", + "reset": "Nustatyti iš naujo", + "show-more": "Rodyti daugiau", + "dont-show-again": "Daugiau neberodyti", + "see-documentation": "Žiūrėti dokumentacijoje", + "clear": "Išvalyti", + "upload": "Įkelti", + "delete-anyway": "Trinti vistike", + "delete-selected": "Trinti pasirinktą", + "set": "Nustatyti" + }, + "aggregation": { + "aggregation": "Agregavimas", + "function": "Duomenų aggregavimo funkcija", + "limit": "Maksimali reikšmė", + "group-interval": "Grupavimo intervalas", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "sum": "Sum", + "count": "Kiekis", + "none": "Nėra" + }, + "admin": { + "settings": "Nustatymai", + "general": "Bendrieji", + "general-settings": "Bendrieji nustatymai", + "home-settings": "Pagrindinio puslapio nustatymai", + "home": "Pagrindinis puslapis", + "outgoing-mail": "Pašto serveris", + "outgoing-mail-settings": "Išeinančio pašto serverio nustatymai", + "system-settings": "Sistemos nustatymai", + "test-mail-sent": "Bandomasis laiškas sėkmingai išsiųstas!", + "base-url": "Pagrindinis URL", + "base-url-required": "Pagrindinis URL yra privalomas.", + "prohibit-different-url": "Drausti naudoti kliento užklausų antraštėse nurodytą pagrindinio kompiuterio pavadinimą", + "prohibit-different-url-hint": "Šis nustatymas turėtų būti įjungtas gamybinėje aplinkoje. Jo išjungimas gali sukelti saugumo problemų", + "device-connectivity": { + "device-connectivity": "Įrenginių jungiamumas", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "Jei laukeliai 'host' arba 'port' yra tušti, bus naudojamos numatytosios protokolo reikšmės.", + "host": "Host", + "port": "Port", + "port-pattern": "Port turi būti teigiamas sveikasis skaičius.", + "port-range": "Port turi būti nuo 1 iki 65535." }, - "api-usage": { - "api-features": "API features", - "api-usage": "Api Usage", - "alarm": "Alarm", - "alarms-created": "Alarms created", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-hourly-activity": "Alarms created hourly activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", - "data-points": "Data points", - "data-points-storage-days": "Data points storage days", - "device-api": "Device API", - "email": "Email", - "email-messages": "Email messages", - "email-messages-daily-activity": "Email messages daily activity", - "email-messages-monthly-activity": "Email messages monthly activity", - "exceptions": "Exceptions", - "executions": "Executions", - "javascript": "JavaScript", - "javascript-executions": "JavaScript executions", - "javascript-functions": "JavaScript functions", - "javascript-functions-daily-activity": "JavaScript functions daily activity", - "javascript-functions-hourly-activity": "JavaScript functions hourly activity", - "javascript-functions-monthly-activity": "JavaScript functions monthly activity", - "latest-error": "Latest Error", - "messages": "Messages", - "notifications": "Notifications", - "notifications-email-sms": "Notifications (Email/SMS)", - "notifications-hourly-activity": "Notifications hourly activity", - "permanent-failures": "${entityName} Permanent Failures", - "permanent-timeouts": "${entityName} Permanent Timeouts", - "processing-failures": "${entityName} Processing Failures", - "processing-failures-and-timeouts": "Processing Failures and Timeouts", - "processing-timeouts": "${entityName} Processing Timeouts", - "queue-stats": "Queue Stats", - "rule-chain": "Rule Chain", - "rule-engine": "Rule Engine", - "rule-engine-daily-activity": "Rule Engine daily activity", - "rule-engine-executions": "Rule Engine executions", - "rule-engine-hourly-activity": "Rule Engine hourly activity", - "rule-engine-monthly-activity": "Rule Engine monthly activity", - "rule-engine-statistics": "Rule Engine Statistics", - "rule-node": "Rule Node", - "sms": "SMS", - "sms-messages": "SMS messages", - "sms-messages-daily-activity": "SMS messages daily activity", - "sms-messages-monthly-activity": "SMS messages monthly activity", - "successful": "${entityName} Successful", - "telemetry": "Telemetry", - "telemetry-persistence": "Telemetry persistence", - "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", - "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", - "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", - "transport": "Transport", - "transport-daily-activity": "Transport daily activity", - "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", - "view-details": "View details", - "view-statistics": "View statistics" - }, - "api-limit": { - "cassandra-queries": "Cassandra queries", - "entity-version-creation": "Entity version creation", - "entity-version-load": "Entity version load", - "integration-messages": "Integration messages", - "integration-messages-per-device": "Integration messages per device", - "notification-requests": "Notification requests", - "notification-requests-per-rule": "Notification requests per rule", - "reports-generation": "Reports generation", - "rest-api-requests": "REST API requests", - "rest-api-requests-per-customer": "REST API requests per customer", - "transport-messages": "Transport messages", - "transport-messages-per-device": "Transport messages per device", - "ws-updates-per-session": "WS updates per session" - }, - "audit-log": { - "audit": "Auditas", - "audit-logs": "Audito žurnalas", - "timestamp": "Laiko žyma", - "entity-type": "Subjektas", - "entity-name": "Pavadinimas", - "user": "Vartotojas", - "type": "Veiksmas", - "status": "Statusas", - "details": "Informacija", - "type-added": "Pridėta", - "type-deleted": "Pašalinta", - "type-updated": "Atnaujinta", - "type-attributes-updated": "Attributai atnaujinti", - "type-attributes-deleted": "Atributai pašalinti", - "type-rpc-call": "RPC iškvietimas", - "type-credentials-updated": "Įgaliojimai atnaujinti", - "type-assigned-to-customer": "Priskirta klientui", - "type-unassigned-from-customer": "Atsieta nuo kliento", - "type-assigned-to-edge": "Assigned to Edge", - "type-unassigned-from-edge": "Unassigned from Edge", - "type-activated": "Aktyvuota", - "type-suspended": "Sustabdyta", - "type-credentials-read": "Įgaliojimų skaitymas", - "type-attributes-read": "Atributų skaitymas", - "type-added-to-entity-group": "Įtraukta į grupę", - "type-removed-from-entity-group": "Pašalinta iš grupės", - "type-relation-add-or-update": "ryšys atnaujintas", - "type-relation-delete": "Ryšys panaikintas", - "type-relations-delete": "Visi ryšiai panaikinti", - "type-alarm-ack": "Pripažinta", - "type-alarm-clear": "Išvalyta", - "type-alarm-assign": "Priskitas", - "type-alarm-unassign": "Atsietas", - "type-added-comment": "Pridėtas komentaras", - "type-updated-comment": "Atnaujintas komentaras", - "type-deleted-comment": "Pašalintas komentaras", - "type-rest-api-rule-engine-call": "Rule engine REST API call", - "type-made-public": "Padaryti viešu", - "type-made-private": "Padaryti privačiu", - "type-login": "Prisijungti", - "type-logout": "Atsijungti", - "type-lockout": "Užblokuoti", - "status-success": "Sėkmė", - "status-failure": "Nesėkmė", - "audit-log-details": "Informacija apie autido žurnalą", - "no-audit-logs-prompt": "Žurnalo įrašų nėra", - "action-data": "Informacija apie veiksmus", - "failure-details": "Informacija apie nesėkmę", - "search": "Žurnalo įrašų paieška", - "clear-search": "Išvalyti paiešką", - "type-assigned-from-tenant": "Priskirta valdytojo", - "type-assigned-to-tenant": "Priskirta valdytojui", - "type-provision-success": "Device provisioned", - "type-provision-failure": "Device provisioning was failed", - "type-timeseries-updated": "Telemetrija atnaujinta", - "type-timeseries-deleted": "Telemetrija pašalinta", - "type-owner-changed": "Savininkas pakeistas", - "type-sms-sent": "SMS sent" - }, - "confirm-on-exit": { - "message": "Turite neišsaugotų pakeitimų. Ar tikrai norite palikti šį puslapį?", - "html-message": "Turite neišsaugotų pakeitimų.
    Ar tikrai norite palikti šį puslapį?", - "title": "Neišsaugoti pakeitimai" - }, - "contact": { - "country": "Šalis", - "city": "Miestas", - "state": "Rajonas", - "postal-code": "Pašto kodas", - "postal-code-invalid": "Neteisingas pašto kodo formatas.", - "address": "Adresas", - "address2": "Adresas 2", - "phone": "Telefonas", + "mail-from": "Laiškas nuo", + "mail-from-required": "Laukas „Laiškas nuo“ yra privalomas.", + "smtp-protocol": "SMTP protokolas", + "smtp-host": "SMTP serveris", + "smtp-host-required": "SMTP serveris yra privalomas.", + "smtp-port": "SMTP prievadas", + "smtp-port-required": "Turite nurodyti SMTP prievadą.", + "smtp-port-invalid": "Tai neatrodo kaip tinkamas SMTP prievadas.", + "timeout-msec": "Laiko limitas (msek)", + "timeout-required": "Laiko limitas yra privalomas.", + "timeout-invalid": "Tai neatrodo kaip tinkamas laiko limitas.", + "enable-tls": "Įjungti TLS", + "tls-version": "TLS versija", + "enable-proxy": "Įjungti proxy", + "proxy-host": "Proxy serveris", + "proxy-host-required": "Proxy serveris yra privalomas.", + "proxy-port": "Proxy prievadas", + "proxy-port-required": "Proxy prievadas yra privalomas.", + "proxy-port-range": "Proxy prievadas turi būti nuo 1 iki 65535.", + "proxy-user": "Proxy naudotojas", + "proxy-password": "Proxy slaptažodis", + "change-password": "Pakeisti slaptažodį", + "send-test-mail": "Siųsti bandomąjį laišką", + "sms-provider": "SMS tiekėjas", + "sms-provider-settings": "SMS tiekėjo nustatymai", + "sms-provider-type": "SMS tiekėjo tipas", + "sms-provider-type-required": "SMS tiekėjo tipas yra privalomas.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS prieigos raktas (Access Key ID)", + "aws-access-key-id-required": "AWS prieigos raktas (Access Key ID) yra privalomas", + "aws-secret-access-key": "AWS slaptas prieigos raktas (Secret Access Key)", + "aws-secret-access-key-required": "AWS slaptas prieigos raktas (Secret Access Key) yra privalomas", + "aws-region": "AWS regionas", + "aws-region-required": "AWS regionas yra privalomas", + "number-from": "Telefono numeris nuo", + "number-from-required": "Telefono numeris nuo yra privalomas.", + "number-to": "Telefono numeris kam", + "number-to-required": "Telefono numeris kam yra privalomas.", + "phone-number-hint": "Telefono numeris E.164 formatu, pvz. +19995550123", + "phone-number-hint-twilio": "Telefono numeris E.164 formatu / Telefono numerio SID / Žinučių paslaugos SID, pvz. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "Neteisingas telefono numeris. Turi būti E.164 formatu, pvz. +19995550123.", + "phone-number-pattern-twilio": "Neteisingas telefono numeris. Turi būti E.164 formatu / Telefono numerio SID / Žinučių paslaugos SID, pvz. +19995550123/PNXXX/MGXXX.", + "sms-message": "SMS žinutė", + "sms-message-required": "SMS žinutė yra privaloma.", + "sms-message-max-length": "SMS žinutė negali būti ilgesnė nei 1600 simbolių", + "twilio-account-sid": "Twilio paskyros SID", + "twilio-account-sid-required": "Twilio paskyros SID yra privalomas", + "twilio-account-token": "Twilio paskyros prieigos raktas (Token)", + "twilio-account-token-required": "Twilio paskyros prieigos raktas (Token) yra privalomas", + "send-test-sms": "Siųsti bandomąją SMS", + "test-sms-sent": "Bandomoji SMS sėkmingai išsiųsta!", + "security-settings": "Saugumo nustatymai", + "password-policy": "Slaptažodžių politika", + "minimum-password-length": "Minimalus slaptažodžio ilgis", + "minimum-password-length-required": "Minimalus slaptažodžio ilgis yra privalomas", + "minimum-password-length-range": "Minimalus slaptažodžio ilgis turi būti nuo 5 iki 50 simbolių", + "maximum-password-length": "Maksimalus slaptažodžio ilgis", + "maximum-password-length-min": "Maksimalus slaptažodžio ilgis turi būti bent 6 simboliai", + "maximum-password-length-less-min": "Maksimalus slaptažodžio ilgis turi būti didesnis nei minimalus", + "minimum-uppercase-letters": "Minimalus didžiųjų raidžių kiekis", + "minimum-uppercase-letters-range": "Minimalus didžiųjų raidžių kiekis negali būti neigiamas", + "minimum-lowercase-letters": "Minimalus mažųjų raidžių kiekis", + "minimum-lowercase-letters-range": "Minimalus mažųjų raidžių kiekis negali būti neigiamas", + "minimum-digits": "Minimalus skaitmenų kiekis", + "minimum-digits-range": "Minimalus skaitmenų kiekis negali būti neigiamas", + "minimum-special-characters": "Minimalus specialiųjų simbolių kiekis", + "minimum-special-characters-range": "Minimalus specialiųjų simbolių kiekis negali būti neigiamas", + "password-expiration-period-days": "Slaptažodžio galiojimo laikotarpis dienomis", + "password-expiration-period-days-range": "Slaptažodžio galiojimo laikotarpis dienomis negali būti neigiamas", + "password-reuse-frequency-days": "Slaptažodžio pakartotinio naudojimo dažnis dienomis", + "password-reuse-frequency-days-range": "Slaptažodžio pakartotinio naudojimo dažnis dienomis negali būti neigiamas", + "allow-whitespace": "Leisti tarpus", + "force-reset-password-if-no-valid": "Priverstinai atstatyti slaptažodį, jei jis negalioja", + "force-reset-password-if-no-valid-hint": "Būkite atsargūs įjungdami šią funkciją: naudotojai, kurių slaptažodžiai netinkami, turės juos atstatyti per el. paštą.", + "general-policy": "Bendroji politika", + "max-failed-login-attempts": "Maksimalus neteisingų prisijungimų skaičius prieš paskyros užrakinimą", + "minimum-max-failed-login-attempts-range": "Maksimalus neteisingų prisijungimų skaičius negali būti neigiamas", + "user-lockout-notification-email": "Naudotojo paskyros užrakinimo atveju siųsti pranešimą el. paštu", + "user-activation-token-ttl": "Naudotojo aktyvavimo nuorodos TTL valandomis", + "user-activation-token-ttl-range": "Naudotojo aktyvavimo nuorodos TTL turi būti nuo 1 iki 24 valandų", + "password-reset-token-ttl": "Slaptažodžio atstatymo nuorodos TTL valandomis", + "password-reset-token-ttl-range": "Slaptažodžio atstatymo nuorodos TTL turi būti nuo 1 iki 24 valandų", + "mobile-secret-key-length": "Mobiliojo slaptosios rakto ilgis", + "mobile-secret-key-length-range": "Mobiliojo slaptosios rakto ilgis turi būti teigiamas", + "domain-name": "Domeno pavadinimas", + "domain-name-unique": "Domeno pavadinimas ir protokolas turi būti unikalūs.", + "domain-name-max-length": "Domeno pavadinimas turi būti trumpesnis nei 256 simboliai", + "error-verification-url": "Domeno pavadinime neturi būti simbolių '/' ir ':'. Pavyzdys: thingsboard.io", + "connection-settings": "Ryšio nustatymai", + "oauth2": { + "access-token-uri": "Prieigos rakto URI", + "access-token-uri-required": "Prieigos rakto URI yra privalomas.", + "activate-user": "Aktyvuoti naudotoją", + "add-domain": "Pridėti domeną", + "delete-domain": "Ištrinti domeną", + "add-provider": "Pridėti tiekėją", + "delete-provider": "Ištrinti tiekėją", + "allow-user-creation": "Leisti naudotojų kūrimą", + "always-fullscreen": "Visada viso ekrano režimu", + "authorization-uri": "Autorizacijos URI", + "authorization-uri-required": "Autorizacijos URI yra privalomas.", + "add-client": "Pridėti OAuth 2.0 klientą", + "client-details": "OAuth 2.0 kliento informacija", + "client": "OAuth 2.0 klientas", + "clients": "OAuth 2.0 klientai", + "no-oauth2-clients": "OAuth 2.0 klientų nerasta", + "search-oauth2-clients": "Ieškoti OAuth 2.0 klientų", + "delete-client-title": "Ar tikrai norite ištrinti OAuth 2.0 klientą '{{clientName}}'?", + "delete-client-text": "Būkite atsargūs, po patvirtinimo klientas ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-mobile-app-title": "Ar tikrai norite ištrinti mobiliojoje programėlėje '{{applicationName}}'?", + "delete-mobile-app-text": "Būkite atsargūs, po patvirtinimo mobilioji programėlė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "title": "Pavadinimas.", + "client-title-required": "Pavadinimas yra privalomas.", + "client-title-max-length": "Pavadinimas turi būti mažiau nei 100", + "advanced-settings": "Išplėstiniai nustatymai", + "domain-details": "Domeno informacija", + "no-domains": "Domenų nerasta.", + "search-domains": "Ieškoti domenų.", + "mobile-app-details": "Mobiliosios programėlės informacija", + "add-mobile-app": "Pridėti mobiliąją programėlę", + "no-mobile-apps": "No applications configured", + "search-mobile-apps": "Ieškoti mobiliųjų programėlių.", + "send-token": "Siųsti prieigos raktą.", + "create-new": "Sukurti naują", + "client-authentication-method": "Kliento autentifikavimo metodas", + "client-id": "Kliento ID", + "client-id-required": "Kliento ID yra privalomas.", + "client-id-max-length": "Kliento ID turi būti trumpesnis nei 256 simboliai", + "client-secret": "Kliento slaptas raktas", + "client-secret-required": "Kliento slaptas raktas yra privalomas.", + "client-secret-max-length": "Kliento slaptis turi būti trumpesnė nei 2049 simboliai", + "custom-setting": "Pasirinktiniai nustatymai.", + "customer-name-pattern": "Kliento vardo šablonas.", + "customer-name-pattern-max-length": "Kliento vardo šablonas turi būti trumpesnis nei 256 simboliai.", + "default-dashboard-name": "Numatytas prietaisų skydelio pavadinimas", + "default-dashboard-name-max-length": "Numatytojo prietaisų skydelio pavadinimas turi būti trumpesnis nei 256 simboliai", + "delete-domain-text": "Būkite atsargūs — po patvirtinimo domenas ir visi tiekėjo duomenys taps nepasiekiami.", + "delete-domain-title": "Ar tikrai norite ištrinti domeną „{{domainName}}“?", + "delete-registration-text": "Būkite atsargūs, po patvirtinimo tiekėjo duomenys taps nepasiekiami.", + "delete-registration-title": "Ar tikrai norite ištrinti tiekėją „{{name}}“?", + "email-attribute-key": "Email attribute key", + "email-attribute-key-required": "Email attribute key is required.", + "email-attribute-key-max-length": "Email attribute key should be less than 32", + "first-name-attribute-key": "First name attribute key", + "first-name-attribute-key-max-length": "First name attribute key should be less than 32", + "general": "General", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Last name attribute key", + "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", + "login-button-icon": "Login button icon", + "login-button-label": "Provider label", + "login-button-label-placeholder": "Login with $(Provider label)", + "login-button-label-required": "Label is required.", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "New domain", + "oauth2": "OAuth2", + "password-max-length": "Password should be less than 256", + "redirect-uri-template": "Redirect URI template", + "copy-redirect-uri": "Nukopijuoti nukreipimo URI.", + "registration-id": "Registration ID", + "registration-id-required": "Registration ID is required.", + "registration-id-unique": "Registration ID need to unique for the system.", + "scope": "Scope", + "scope-required": "Scope is required.", + "tenant-name-pattern": "Tenant name pattern", + "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", + "tenant-name-strategy": "Tenant name strategy", + "type": "Mapper type", + "uri-pattern-error": "Invalid URI format.", + "url": "URL", + "url-pattern": "Invalid URL format.", + "url-required": "URL is required.", + "url-max-length": "URL should be less than 256", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI is required.", + "username-max-length": "User name should be less than 256", + "user-name-attribute-name": "User name attribute key", + "user-name-attribute-name-required": "User name attribute key is required", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Enable OAuth2 settings", + "disable": "Išjungti OAuth 2.0 nustatymus", + "edge": "Propaguoti į Edge", + "edge-enable": "Įjungti propagavimą į „Edge“", + "edge-disable": "Išjungti propagavimą į „Edge“", + "domains": "Domains", + "mobile-apps": "Mobile applications", + "mobile-package": "Application package", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", + "mobile-package-unique": "Application package must be unique.", + "mobile-package-required": "Programėlės paketo pavadinimas yra privalomas.", + "mobile-package-max-length": "Programėlės paketo pavadinimas turi būti trumpesnis nei 256 simboliai.", + "mobile-package-spaces": "Programėlės paketo pavadinimas negali turėti tarpų.", + "mobile-app-secret": "Application secret", + "mobile-app-secret-hint": "Base64 koduota eilutė, vaizduojanti bent 512 bitų duomenų.", + "mobile-app-secret-required": "Programėlės slaptasis raktas yra privalomas.", + "mobile-app-secret-min-length": "Programėlės slaptasis raktas turi būti bent 512 bitų duomenų.", + "mobile-app-secret-base64": "Programos paslaptis turi būti „base64“ formato.", + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", + "copy-mobile-app-secret": "Nukopijuoti programos slaptą raktą", + "delete-mobile-app": "Ištrinti programos informaciją", + "providers": "Providers", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "Visos platformos", + "smtp-provider": "SMTP provider", + "allowed-platforms": "Leistinos platforms", + "authentication": "Autentifikacija", + "basic": "Bazinis", + "provider": "Provider", + "redirect-url": "Redirect URI", + "domain-name": "Domeno pavadinimas", + "domain-name-required": "Domeno pavadinimas yra privalomas", + "redirect-url-template": "Redirect URI template", + "microsoft-tenant-id": "Directory (tenant) Id", + "microsoft-tenant-id-required": "Directory (tenant) Id is required", + "token-uri": "Token URI", + "token-uri-required": "Token URI is required", + "redirect-uri": "Redirect URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "Pasirinktinis", + "generate-access-token": "Generate access token", + "update-access-token": "Update access token", + "access-token-status": "Prieigos rakto būsena:", + "token-status-generated": "generated", + "token-status-not-generated": "not generated" + }, + "smpp-provider": { + "smpp-version": "SMPP versija", + "smpp-host": "SMPP serveris", + "smpp-host-required": "SMPP serveris yra privalomas", + "smpp-port": "SMPP prievadas", + "smpp-port-required": "SMPP prievadas yra privalomas", + "system-id": "Sistemos ID", + "system-id-required": "Sistemos ID yra privalomas", + "password": "Slaptažodis", + "password-required": "Slaptažodis yra privalomas", + "type-settings": "Tipo nustatymai", + "source-settings": "Šaltinio nustatymai", + "destination-settings": "Paskirties nustatymai", + "additional-settings": "Papildomi nustatymai", + "system-type": "Sistemos tipas", + "bind-type": "Susiejimo tipas", + "service-type": "Paslaugos tipas", + "source-address": "Šaltinio adresas", + "source-ton": "Šaltinio TON", + "source-npi": "Šaltinio NPI", + "destination-ton": "Paskirties TON (Numerio tipas)", + "destination-npi": "Paskirties NPI (Numeracijos plano identifikacija)", + "address-range": "Adreso diapazonas", + "coding-scheme": "Koduotės schema", + "bind-type-tx": "Siuntėjas", + "bind-type-rx": "Gavėjas", + "bind-type-trx": "Siuntėjas/Gavėjas (Transceiver)", + "ton-unknown": "Nežinomas", + "ton-international": "Tarptautinis", + "ton-national": "Nacionalinis", + "ton-network-specific": "Tinklo specifinis", + "ton-subscriber-number": "Abonento numeris", + "ton-alphanumeric": "Raidinis-skaitmeninis", + "ton-abbreviated": "Sutrumpintas", + "npi-unknown": "0 - Nežinomas", + "npi-isdn": "1 - ISDN/telefono numeracijos planas (E163/E164)", + "npi-data-numbering-plan": "3 - Duomenų numeracijos planas (X.121)", + "npi-telex-numbering-plan": "4 - Telex numeracijos planas (F.69)", + "npi-land-mobile": "6 - Mobilusis (E.212)", + "npi-national-numbering-plan": "8 - Nacionalinis numeracijos planas", + "npi-private-numbering-plan": "9 - Privatus numeracijos planas", + "npi-ermes-numbering-plan": "10 - ERMES numeracijos planas (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internetas (IP)", + "npi-wap-client-id": "18 - WAP kliento ID (nustatomas WAP forumo)", + "scheme-smsc": "0 - SMSC numatytoji abėcėlė (ASCII trumpiems ir ilgiems kodams bei GSM be mokesčio numeriams)", + "scheme-ia5": "1 - IA5 (ASCII trumpiems ir ilgiems kodams, Latin 9 be mokesčio numeriams (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Neapibrėžtas aštuonetas (8 bitų dvejetainis)", + "scheme-latin-1": "3 - Lotynų 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Neapibrėžtas aštuonetas (8 bitų dvejetainis)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Kirilica (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Lotynų/Hebrajų (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Piktogramų koduotė", + "scheme-music-codes": "10 - Muzikos kodai (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Išplėstinė Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Korėjiečių grafinis simbolių rinkinys (KS C 5601/KS X 1001)" + }, + "queue-select-name": "Pasirinkite eilės pavadinimą", + "queue-name": "Pavadinimas", + "queue-name-required": "Eilės pavadinimas yra privalomas!", + "queues": "Eilės", + "queue-partitions": "Skirsniai", + "queue-submit-strategy": "Pateikimo strategija", + "queue-processing-strategy": "Apdorojimo strategija", + "queue-configuration": "Eilės konfigūracija", + "repository-settings": "Repozitorijos nustatymai", + "repository": "Repozitorija", + "repository-url": "Repozitorijos URL", + "repository-url-required": "Repozitorijos URL yra privalomas.", + "default-branch": "Numatytasis šakos pavadinimas", + "repository-read-only": "Tik skaitymui", + "show-merge-commits": "Rodyti sujungimo (merge) įrašus", + "authentication-settings": "Autentifikavimo nustatymai", + "auth-method": "Autentifikavimo metodas", + "auth-method-username-password": "Slaptažodis / prieigos raktas", + "auth-method-username-password-hint": "GitHub naudotojai privalo naudoti prieigos raktus su rašymo teisėmis į repozitoriją.", + "auth-method-private-key": "Privatus raktas", + "password-access-token": "Slaptažodis / prieigos raktas", + "change-password-access-token": "Pakeisti slaptažodį / prieigos raktą", + "private-key": "Privatus raktas", + "drop-private-key-file-or": "Vilkite ir numeskite privataus rakto failą arba", + "passphrase": "Slaptažodžio frazė", + "enter-passphrase": "Įveskite slaptažodžio frazę", + "change-passphrase": "Pakeisti slaptažodžio frazę", + "check-access": "Patikrinti prieigą", + "check-repository-access-success": "Repozitorijos prieiga sėkmingai patvirtinta!", + "delete-repository-settings-title": "Ar tikrai norite ištrinti repozitorijos nustatymus?", + "delete-repository-settings-text": "Būkite atsargūs, po patvirtinimo repozitorijos nustatymai bus pašalinti ir versijų valdymo funkcija taps nepasiekiama.", + "auto-commit-settings": "Automatinio įrašo nustatymai", + "auto-commit": "Automatinis įrašas (commit)", + "auto-commit-entities": "Automatinio įrašo objektai", + "no-auto-commit-entities-prompt": "Nėra sukonfigūruotų objektų automatiniam įrašui", + "delete-auto-commit-settings-title": "Ar tikrai norite ištrinti automatinio įrašo nustatymus?", + "delete-auto-commit-settings-text": "Būkite atsargūs, po patvirtinimo automatinio įrašo nustatymai bus pašalinti ir funkcija bus išjungta visiems objektams.", + "mobile-app": { + "mobile-app": "Mobilioji programėlė", + "mobile-app-qr-code-widget-settings": "Mobiliosios programėlės QR kodo valdiklio nustatymai", + "applications": "Programėlės", + "default": "Numatytasis", + "custom": "Pasirinktinis", + "android": "Android", + "ios": "iOS", + "appearance": "Išvaizda", + "appearance-on-home-page": "Išvaizda pagrindiniame puslapyje", + "enabled": "Įjungta", + "disabled": "Išjungta", + "badges": "Ženkleliai", + "label": "Etiketė", + "label-required": "Etiketė yra privaloma", + "label-max-length": "Etiketė turi būti ne ilgesnė kaip 50 simbolių", + "right": "Dešinėje", + "left": "Kairėje", + "set": "Nustatyti", + "preview": "Peržiūra", + "connect-mobile-app": "Prijungti mobiliąją programėlę", + "use-system-settings": "Naudoti sistemos nustatymus" + }, + "2fa": { + "2fa": "Dvigubas autentifikavimas", + "available-providers": "Galimi tiekėjai", + "issuer-name": "Leidėjo pavadinimas", + "issuer-name-required": "Leidėjo pavadinimas yra privalomas.", + "max-verification-failures-before-user-lockout": "Maksimalus neteisingų patvirtinimų skaičius prieš naudotojo užrakinimą", + "max-verification-failures-before-user-lockout-pattern": "Maksimalus patvirtinimų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-checking-attempts": "Tikrinimo bandymų skaičius", + "number-of-checking-attempts-pattern": "Tikrinimo bandymų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-checking-attempts-required": "Tikrinimo bandymų skaičius yra privalomas.", + "number-of-codes": "Kodų skaičius", + "number-of-codes-pattern": "Kodų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-codes-required": "Kodų skaičius yra privalomas.", + "provider": "Tiekėjas", + "retry-verification-code-period": "Pakartotinio patvirtinimo kodo laikotarpis (sek)", + "retry-verification-code-period-pattern": "Minimalus laikotarpis yra 5 sek.", + "retry-verification-code-period-required": "Pakartotinio patvirtinimo kodo laikotarpis yra privalomas.", + "total-allowed-time-for-verification": "Bendras leidžiamas patvirtinimo laikas (sek)", + "total-allowed-time-for-verification-pattern": "Minimalus bendras leidžiamas laikas yra 60 sek.", + "total-allowed-time-for-verification-required": "Bendras leidžiamas laikas yra privalomas.", + "use-system-two-factor-auth-settings": "Naudoti sistemos dviejų veiksnių autentifikavimo nustatymus", + "verification-code-check-rate-limit": "Patvirtinimo kodo tikrinimo dažnio limitas", + "verification-code-lifetime": "Patvirtinimo kodo galiojimo laikas (sek)", + "verification-code-lifetime-pattern": "Patvirtinimo kodo galiojimo laikas turi būti teigiamas sveikasis skaičius.", + "verification-code-lifetime-required": "Patvirtinimo kodo galiojimo laikas yra privalomas.", + "verification-message-template": "Patvirtinimo pranešimo šablonas", + "verification-limitations": "Patvirtinimo apribojimai", + "verification-message-template-pattern": "Patvirtinimo pranešime turi būti šablonas: ${code}", + "verification-message-template-required": "Patvirtinimo pranešimo šablonas yra privalomas.", + "within-time": "Per laiką (sek)", + "within-time-pattern": "Laikas turi būti teigiamas sveikasis skaičius.", + "within-time-required": "Laikas yra privalomas." + }, + "jwt": { + "security-settings": "JWT saugumo nustatymai", + "issuer-name": "Leidėjo pavadinimas", + "issuer-name-required": "Leidėjo pavadinimas yra privalomas.", + "signings-key": "Pasirašymo raktas", + "signings-key-hint": "Base64 koduota eilutė, atitinkanti bent 256 bitų duomenis.", + "signings-key-required": "Pasirašymo raktas yra privalomas.", + "signings-key-min-length": "Pasirašymo raktas turi būti bent 256 bitų duomenų ilgio.", + "signings-key-base64": "Pasirašymo raktas turi būti Base64 formatu.", + "expiration-time": "Žetono galiojimo laikas (sek)", + "expiration-time-required": "Žetono galiojimo laikas yra privalomas.", + "expiration-time-max": "Didžiausias leidžiamas laikas – 2147483647 sekundės (68 metai).", + "expiration-time-min": "Mažiausias laikas – 60 sekundžių (1 minutė).", + "refresh-expiration-time": "Atnaujinimo žetono galiojimo laikas (sek)", + "refresh-expiration-time-required": "Atnaujinimo žetono galiojimo laikas yra privalomas.", + "refresh-expiration-time-max": "Didžiausias leidžiamas laikas – 2147483647 sekundės (68 metai).", + "refresh-expiration-time-min": "Mažiausias laikas – 900 sekundžių (15 minučių).", + "refresh-expiration-time-less-token": "Atnaujinimo žetono laikas turi būti ilgesnis už pagrindinio žetono laiką.", + "generate-key": "Generuoti raktą", + "info-header": "Visi naudotojai turės prisijungti iš naujo", + "info-message": "JWT pasirašymo rakto pakeitimas panaikins visų išduotų žetonų galiojimą. Visi naudotojai turės prisijungti iš naujo. Tai taip pat paveiks scenarijus, naudojančius Rest API / Websockets." + }, + "resources": "Ištekliai", + "notifications": "Pranešimai", + "notifications-settings": "Pranešimų nustatymai", + "slack-api-token": "Slack API prieigos raktas", + "slack": "Slack", + "slack-settings": "Slack nustatymai", + "mobile-settings": "Mobiliosios programos nustatymai", + "firebase-service-account-file": "Firebase paslaugos paskyros kredencialų JSON failas", + "select-firebase-service-account-file": "Vilkite ir numeskite savo Firebase paslaugos paskyros kredencialų failą arba ", + "trendz": "PredAna", + "trendz-settings": "PredAna nustatymai", + "trendz-url": "PredAna URL", + "trendz-url-required": "PredAna URL yra privalomas", + "trendz-api-key": "PredAna API raktas", + "trendz-enable": "Įjungti PredAna" + }, + "alarm": { + "alarm": "Įspėjimas", + "alarms": "Įspėjimai", + "all-alarms": "Visi įspėjimai", + "select-alarm": "Pasirinkite įspėjimą", + "no-alarms-matching": "Su '{{entity}}' susijusių įspėjimų nerasta.", + "alarm-required": "Įspėjimas yra privalomas", + "alarm-filter": "Įspėjimų filtras", + "filter": "Filtras", + "alarm-status": "Įspėjimo būsena", + "alarm-status-list": "Įspėjimo būsenų sąrašas", + "any-status": "Visos būsenos", + "search-status": { + "ANY": "Visi", + "ACTIVE": "Aktyvūs", + "CLEARED": "Išvalyti", + "ACK": "Patvirtinti", + "UNACK": "Nepatvirtinti" + }, + "display-status": { + "ACTIVE_UNACK": "Aktyvūs nepatvirtinti", + "ACTIVE_ACK": "Aktyvūs pripažinti", + "CLEARED_UNACK": "Atmesti nepatvirtinti", + "CLEARED_ACK": "Atmesti pripažinti" + }, + "no-alarms-prompt": "Įspėjimų nėra", + "created-time": "Sukūrimo laikas", + "type": "Tipas", + "severity": "Lygis", + "originator": "Iniciatorius", + "originator-type": "Iniciatoriaus tipas", + "details": "Pastabos", + "originator-label": "Iniciatoriaus pavadinimas", + "assign": "Priskirti", + "assignments": "Priskyrimai", + "assignee": "Atsakingas", + "assignee-id": "Atsakingo asmens ID", + "assignee-first-name": "Atsakingo asmens vardas", + "assignee-last-name": "Atsakingo asmens pavardė", + "assignee-email": "Atsakingo asmens e-paštas", + "unassigned": "Panaikinti priskyrimą", + "user-deleted": "Vartotojas ištrintas", + "assignee-not-set": "Visi", + "status": "Statusas", + "alarm-details": "Įspėjimo pastabos", + "start-time": "Pradžia", + "assign-time": "Priskyrimo laikas", + "end-time": "Pabaiga", + "ack-time": "Pripažinimo laikas", + "clear-time": "Atmetimo laikas", + "duration": "Trukmė", + "alarm-severity": "Alarm severity", + "alarm-severity-list": "Įspėjimų lygiai", + "any-severity": "Visi lygiai", + "severity-critical": "Kritinis", + "severity-major": "Svarbus", + "severity-minor": "Antraeilis", + "severity-warning": "Įspėjimas", + "severity-indeterminate": "Nenustatytas", + "acknowledge": "Patvirtinti", + "clear": "Atmesti", + "delete": "Panaikinti", + "search": "Įspėjimo paieška", + "selected-alarms": "Pasirinkta { count, plural, =1 {1 įspėjimas} other {# įspėjimai} }", + "no-data": "Nėra duomenų", + "polling-interval": "Įspėjimų tikrinimo intervalas (sek)", + "polling-interval-required": "Reikalingas įspėjimų tikrinimo intervalas", + "min-polling-interval-message": "Mažiausias įspėjimų tikrinimo intervalas yra 1 sekundė.", + "aknowledge-alarms-title": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "aknowledge-alarms-text": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "aknowledge-alarm-title": "Pripažinti įspėjimą", + "aknowledge-alarm-text": "Pripažinti įspėjimą?", + "selected-alarms-are-acknowledged": "Pasirinkti įspėjimai pripažinti", + "clear-alarms-title": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "clear-alarms-text": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "clear-alarm-title": "Atmesti įspėjimą", + "clear-alarm-text": "Atmesti įspėjimą?", + "delete-alarms-title": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "delete-alarms-text": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "selected-alarms-are-cleared": "Pasirinkti įspėjimai atmesti", + "alarm-status-filter": "Įspėjimų statusai", + "alarm-filter-title": "Įspėjimų filtras", + "assigned": "Priskirta", + "filter-title": "Filtras", + "max-count-load": "Didžiausias rodomų įspėjimų skaičius (0 – neribotas)", + "max-count-load-required": "Reikalingas didžiausias rodomų įspėjimų skaičius (0 – neribotas).", + "max-count-load-error-min": "Mažiausia reikšmė yra 0.", + "fetch-size": "Fetch size", + "fetch-size-required": "Fetch size is required.", + "fetch-size-error-min": "Mažiausia rekšmė yra 10.", + "alarm-types": "Įspėjimų tipai", + "alarm-type-list": "Įspėjimų tipų sąrašas", + "any-type": "Visi tipai", + "assigned-to-current-user": "Priskirta prisijungusiam vartotojui", + "assigned-to-me": "Priskirta man", + "search-propagated-alarms": "Ieškoti išplatintų įspėjimų", + "comments": "Įspėjimo pastabos", + "show-more": "Rodyti daugiau", + "additional-info": "Papildoma informacija", + "alarm-type": "Įspėjimo tipas", + "enter-alarm-type": "Įveskite įspėjimo tipą", + "no-alarm-types-matching": "Nėra įspėjimų tipų, kurie atitiktų '{{entitySubtype}}'.", + "alarm-type-list-empty": "Nepasirinktas įspėjimų tipas" + }, + "alarm-activity": { + "add": "Pridėti komentarą...", + "alarm-comment": "Įspėjimo komentaras", + "comments": "Komentarai", + "delete-alarm-comment": "Ištrinti komentarą?", + "refresh": "Atnaujinti", + "oldest-first": "Seniausi viršuje", + "newest-first": "Naujausi viršuje", + "activity": "Veikla", + "export": "Eksportuoti į CSV", + "author": "Sukūrė", + "created-date": "Sukūrimo dara", + "edited-date": "Redagavimo data", + "text": "Tekstas", + "system": "Sistema" + }, + "alias": { + "add": "Pridėti pseudonimą", + "edit": "Redaguoti pseudonimą", + "name": "Pseudonimas", + "name-required": "Pseudonimas yra privalomas", + "duplicate-alias": "Toks pseudonimas sistemoje jau egzistuoja.", + "filter-type-single-entity": "Vienas subjektas", + "filter-type-entity-list": "Subjektų sąrašas", + "filter-type-entity-name": "Subjekto pavadinimas", + "filter-type-entity-type": "Subjekto tipas", + "filter-type-state-entity": "Subjektas iš skydelio būsenos", + "filter-type-state-entity-description": "Subjektas iš skydelio būsenos parametrų", + "filter-type-asset-type": "Turto tipas", + "filter-type-asset-type-description": "Turtas, kurio tipas '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Turtas, kurio tipas '{{assetTypes}}' ir pavadinimas prasideda '{{prefix}}'", + "filter-type-device-type": "Įrenginio tipas", + "filter-type-device-type-description": "Įrenginiai, kurių tipas '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Įrenginiai, kurių tipas '{{deviceTypes}}' ir kurių pavadinimas prasideda '{{prefix}}'", + "filter-type-entity-view-type": "Subjekto rodinio tipas", + "filter-type-entity-view-type-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}' ir kurių pavadinimas prasideda '{{prefix}}'", + "filter-type-edge-type": "Krašto (Edge) tipas", + "filter-type-edge-type-description": "Kraštai (Edges), kurių tipas '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Kraštai, kurių tipas '{{edgeTypes}}' ir pavadinimas prasideda '{{prefix}}'", + "filter-type-relations-query": "Ryšių užklausa", + "filter-type-relations-query-description": "{{entities}} turintys {{relationType}} ryšį {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Kraštų (Edge) paieškos užklausa", + "filter-type-edge-search-query-description": "Kraštai, kurių tipai {{edgeTypes}}, turintys {{relationType}} ryšį {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Turto paieškos užklausa", + "filter-type-asset-search-query-description": "{{assetTypes}} tipo turtas, turintis {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Įrenginio paieškos užklausa", + "filter-type-device-search-query-description": "{{deviceTypes}} tipo įrenginiai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Subjektų rodinio paieškos užklausa", + "filter-type-entity-view-search-query-description": "{{entityViewTypes}} tipo subjektai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "API naudojimo būsena", + "entity-filter": "Subjektų filtras", + "resolve-multiple": "Apdoroti kelis subjektus vienu metu", + "resolve-multiple-hint": "Įjunkite, jei norite vienu metu atvaizduoti duomenis iš visų filtruotų subjektų.\nJei išjungta, valdiklis rodys duomenis tik iš pasirinkto subjekto.", + "filter-type": "Filtro tipas", + "filter-type-required": "Filtro tipas yra privalomas.", + "entity-filter-no-entity-matched": "Subjektų, atitinkančių filtro kriterijus, nerasta.", + "no-entity-filter-specified": "Subjektų filtras nenustatytas", + "root-state-entity": "Naudoti subjektą iš skydelio būsenos kaip pagrindinį", + "last-level-relation": "Gauti tik paskutinio lygio ryšį", + "root-entity": "Pagrindinis subjektas", + "state-entity-parameter-name": "Būsenos subjekto parametro pavadinimas", + "default-state-entity": "Numatytasis būsenos subjektas", + "default-entity-parameter-name": "Numatytasis parametro pavadinimas", + "query-options": "Užklausos parinktys", + "max-relation-level": "Maksimalus ryšių lygis", + "unlimited-level": "Neribotas lygis", + "state-entity": "Skydelio būsenos subjektas", + "all-entities": "Visi subjektai", + "any-relation": "Visi ryšiai" + }, + "asset": { + "asset": "Turtas", + "assets": "Turtas", + "management": "Turto valdymas", + "view-assets": "Peržiūrėti turtą", + "add": "Pridėti turtą", + "asset-type-max-length": "Turto tipas turi būti trumpesnis nei 256 simboliai", + "assign-to-customer": "Priskirti klientui", + "assign-asset-to-customer": "Priskirti turtą klientui", + "assign-asset-to-customer-text": "Pasirinkite turtą, kurį norite priskirti klientui", + "no-assets-text": "Turtas nerastas", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskiriamas turtas", + "public": "Viešas", + "assignedToCustomer": "Priskirtas klientui", + "make-public": "Neriboti turto matomumo", + "make-private": "Riboti turto matomumą", + "unassign-from-customer": "Atsieti nuo kliento", + "delete": "Pašalinti turtą", + "asset-public": "Turtas yra viešas", + "asset-type": "Turto tipas", + "asset-type-required": "Turto tipas yra privalomas.", + "select-asset-type": "Pasirinkite turto tipą", + "enter-asset-type": "Įveskite turto tipą", + "any-asset": "Bet koks turtas", + "no-asset-types-matching": "Turto tipas, atitinkantis '{{entitySubtype}}', nerastas.", + "asset-type-list-empty": "Nepasirinkti turto tipai.", + "asset-types": "Turto tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "label-max-length": "Etiketė turi būti trumpesnė nei 256 simboliai", + "description": "Aprašymas", + "type": "Tipas", + "type-required": "Tipas yra privalomas.", + "details": "Pastabos", + "events": "Įvykiai", + "add-asset-text": "Pridėti turtą", + "asset-details": "Turto informacija", + "assign-assets": "Priskirti turtą", + "assign-assets-text": "Priskirti { count, plural, =1 {1 turtą} other {# turtus} } klientui", + "assign-asset-to-edge-title": "Priskirti turtą (-us) Edge įrenginiui", + "assign-asset-to-edge-text": "Pasirinkite turtą (-us), kuriuos norite priskirti Edge įrenginiui", + "delete-assets": "Pašalinti turtą", + "unassign-assets": "Atsieti turtą", + "unassign-assets-action-title": "Atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo kliento?", + "assign-new-asset": "Priskirti naują turtą", + "delete-asset-title": "Pašalinti turtą '{{assetName}}'?", + "delete-asset-text": "Būkite atsargūs, po patvirtinimo turtas ir visa su juo susijusi informacija bus pašalinta.", + "delete-assets-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }?", + "delete-assets-action-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }", + "delete-assets-text": "Būkite atsargūs, po patvirtinimo visas turtas ir su juo susijusi informacija bus pašalinta.", + "make-public-asset-title": "Ar tikrai norite neriboti turto '{{assetName}}' matomumo?", + "make-public-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija bus prieinama visiems.", + "make-private-asset-title": "Ar tikrai norite apriboti turto '{{assetName}}' matomumą?", + "make-private-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija nebebus prieinama kitiems.", + "unassign-asset-title": "Ar tikrai norite atsieti turtą '{{assetName}}'?", + "unassign-asset-text": "Po patvirtinimo turtas bus atsietas, o klientas neteks prieigos prie jo.", + "unassign-asset": "Atsieti turtą", + "unassign-assets-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} }?", + "unassign-assets-text": "Po patvirtinimo visas pasirinktas turtas bus atsietas, o klientas neteks prieigos prie jo.", + "copyId": "Kopijuoti turto ID", + "idCopiedMessage": "Turto ID nukopijuotas į iškarpinę", + "select-asset": "Pasirinkite turtą", + "no-assets-matching": "Turtas, atitinkantis '{{entity}}', nerastas.", + "asset-required": "Turtas yra privalomas", + "name-starts-with": "Turto pavadinimas prasideda nuo", + "help-text": "Naudokite simbolį '%' pagal paieškos poreikį: '%turto_fragmentas%', '%turto_pabaiga', 'turto_pradžia%'.", + "search": "Turto paieška", + "import": "Importuoti turtą", + "asset-file": "Turto failas", + "label": "Etiketė", + "assign-asset-to-edge": "Priskirti turtą (-us) Edge įrenginiui", + "unassign-asset-from-edge": "Atsieti turtą nuo Edge", + "unassign-asset-from-edge-title": "Ar tikrai norite atsieti turtą '{{assetName}}' nuo Edge?", + "unassign-asset-from-edge-text": "Po patvirtinimo turtas bus atsietas ir nebebus pasiekiamas Edge įrenginyje.", + "unassign-assets-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo Edge?", + "unassign-assets-from-edge-text": "Po patvirtinimo visi pasirinkti turtai bus atsieti ir nebebus prieinami Edge įrenginiuose.", + "selected-assets": "Pasirinkta { count, plural, =1 {1 turtas} other {# turtai} }" + }, + "attribute": { + "attributes": "Atributai", + "latest-telemetry": "Naujausia telemetrija", + "no-latest-telemetry": "Telemetrijos duomenų nėra", + "attributes-scope": "Subjekto atributų sritis", + "scope-telemetry": "Telemetrija", + "scope-latest-telemetry": "Naujausia telemetrija", + "scope-client": "Kliento atributai", + "scope-server": "Serverio atributai", + "scope-shared": "Bendrinami atributai", + "scope-client-short": "Client", + "scope-server-short": "Server", + "scope-shared-short": "Shared", + "scope-latest-short": "Latest", + "scope-any": "Bet kurie", + "add": "Pridėti atributą", + "key": "Raktas", + "key-max-length": "Raktas negali viršyti 256 simbolių", + "last-update-time": "Paskutinio atnaujinimo laikas", + "key-required": "Atributo raktas yra privalomas.", + "value": "Reikšmė", + "value-required": "Atributo reikšmė yra privaloma.", + "telemetry-key-required": "Telemetrijos raktas yra privalomas.", + "telemetry-value-required": "Telemetrijos reikšmė yra privaloma.", + "delete-attributes-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 atributą} other {# atributus} }?", + "delete-attributes-text": "Būkite atsargūs — po patvirtinimo pasirinkti atributai bus pašalinti.", + "delete-attributes": "Pašalinti atributus", + "enter-attribute-value": "Įveskite atributo reikšmę", + "show-on-widget": "Rodyti valdiklyje", + "widget-mode": "Valdiklio režimas", + "next-widget": "Kitas valdiklis", + "prev-widget": "Ankstesnis valdiklis", + "add-to-dashboard": "Pridėti į skydelį", + "add-widget-to-dashboard": "Pridėti valdiklį į skydelį", + "selected-attributes": "Pasirinkta { count, plural, =1 {1 atributas} other {# atributai} }", + "selected-telemetry": "Pasirinkta { count, plural, =1 {1 telemetrijos įrašas} other {# telemetrijos įrašai} }", + "no-attributes-text": "Atributų nėra", + "no-telemetry-text": "Telemetrijos duomenų nėra", + "copy-key": "Kopijuoti raktą", + "add-telemetry": "Pridėti telemetriją", + "copy-value": "Kopijuoti reikšmę", + "delete-timeseries": { + "start-time": "Pradžios laikas", + "ends-on": "Pabaigos laikas", + "strategy": "Strategija", + "delete-strategy": "Šalinimo strategija", + "all-data": "Pašalinti visus duomenis", + "all-data-except-latest-value": "Pašalinti visus duomenis, išskyrus naujausią reikšmę", + "latest-value": "Pašalinti naujausią reikšmę", + "all-data-for-time-period": "Pašalinti visus duomenis laikotarpiui", + "rewrite-latest-value": "Perrašyti naujausią reikšmę" + } + }, + "api-usage": { + "api-features": "API funkcijos", + "api-usage": "API naudojimas", + "alarm": "Įspėjimas", + "alarms-created": "Sukurti įspėjimai", + "queue-stats": "Eilės statistika", + "processing-failures-and-timeouts": "Apdorojimo klaidos ir laiko limitai", + "exceptions": "Išimtys", + "alarms-created-daily-activity": "Įspėjimų dienos veikla", + "alarms-created-hourly-activity": "Įspėjimų valandinė veikla", + "alarms-created-monthly-activity": "Įspėjimų mėnesinė veikla", + "data-points": "Duomenų taškai", + "data-points-storage-days": "Duomenų taškų saugojimo dienos", + "device-api": "Įrenginio API", + "email": "El. paštas", + "email-messages": "El. laiškai", + "email-messages-daily-activity": "El. laiškų dienos veikla", + "email-messages-monthly-activity": "El. laiškų mėnesinė veikla", + "executions": "Vykdymai", + "scripts": "Skriptai", + "scripts-hourly-activity": "Skriptų valandinė veikla", + "scripts-daily-activity": "Skriptų dienos veikla", + "scripts-monthly-activity": "Skriptų mėnesinė veikla", + "javascript": "JavaScript", + "javascript-executions": "JavaScript vykdymai", + "tbel": "TBEL", + "tbel-executions": "TBEL vykdymai", + "latest-error": "Naujausia klaida", + "messages": "Pranešimai", + "notifications": "Pranešimai", + "notifications-email-sms": "Pranešimai (El. paštas / SMS)", + "notifications-hourly-activity": "Pranešimų valandinė veikla", + "permanent-failures": "${entityName} nuolatinės klaidos", + "permanent-timeouts": "${entityName} nuolatiniai laiko limitai", + "processing-failures": "${entityName} apdorojimo klaidos", + "processing-timeouts": "${entityName} apdorojimo laiko limitai", + "rule-chain": "Taisyklių grandinė (Rule Chain)", + "rule-engine": "Taisyklių variklis (Rule Engine)", + "rule-engine-daily-activity": "Taisyklių variklio dienos veikla", + "rule-engine-executions": "Taisyklių variklio vykdymai", + "rule-engine-hourly-activity": "Taisyklių variklio valandinė veikla", + "rule-engine-monthly-activity": "Taisyklių variklio mėnesinė veikla", + "rule-engine-statistics": "Taisyklių variklio statistika", + "rule-node": "Taisyklių mazgas (Rule Node)", + "sms": "SMS", + "sms-messages": "SMS žinutės", + "sms-messages-daily-activity": "SMS žinučių dienos veikla", + "sms-messages-monthly-activity": "SMS žinučių mėnesinė veikla", + "successful": "${entityName} sėkmingi vykdymai", + "telemetry": "Telemetrija", + "telemetry-persistence": "Telemetrijos įrašymas", + "telemetry-persistence-daily-activity": "Telemetrijos įrašymo dienos veikla", + "telemetry-persistence-hourly-activity": "Telemetrijos įrašymo valandinė veikla", + "telemetry-persistence-monthly-activity": "Telemetrijos įrašymo mėnesinė veikla", + "transport": "Transportas", + "transport-daily-activity": "Transporto dienos veikla", + "transport-data-points": "Transporto duomenų taškai", + "transport-hourly-activity": "Transporto valandinė veikla", + "transport-messages": "Transporto pranešimai", + "transport-monthly-activity": "Transporto mėnesinė veikla", + "view-details": "Peržiūrėti detales", + "view-statistics": "Peržiūrėti statistiką" + }, + "api-limit": { + "cassandra-write-queries-core": "REST API Cassandra rašymo užklausos", + "cassandra-read-queries-core": "REST API ir WS telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-queries-rule-engine": "Taisyklių variklio (Rule Engine) telemetrijos Cassandra rašymo užklausos", + "cassandra-read-queries-rule-engine": "Taisyklių variklio (Rule Engine) telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-queries-monolith": "Monolitinės telemetrijos Cassandra rašymo užklausos", + "cassandra-read-queries-monolith": "Monolitinės telemetrijos Cassandra skaitymo užklausos", + "entity-version-creation": "Subjekto versijos kūrimas", + "entity-version-load": "Subjekto versijos įkėlimas", + "notification-requests": "Pranešimų užklausos", + "notification-requests-per-rule": "Pranešimų užklausos pagal taisyklę", + "rest-api-requests": "REST API užklausos", + "rest-api-requests-per-customer": "REST API užklausos pagal klientą", + "transport-messages": "Transporto pranešimai", + "transport-messages-per-device": "Transporto pranešimai pagal įrenginį", + "transport-messages-per-gateway": "Transporto pranešimai pagal šliuzą (gateway)", + "transport-messages-per-gateway-device": "Transporto pranešimai pagal šliuzo įrenginį", + "ws-updates-per-session": "WS atnaujinimai per sesiją", + "edge-events": "Edge įvykiai", + "edge-events-per-edge": "Edge įvykiai pagal Edge", + "edge-uplink-messages": "Edge uplink pranešimai", + "edge-uplink-messages-per-edge": "Edge uplink pranešimai pagal Edge" + }, + "audit-log": { + "audit": "Auditas", + "audit-logs": "Audito žurnalas", + "timestamp": "Laiko žyma", + "entity-type": "Subjektas", + "entity-name": "Pavadinimas", + "user": "Vartotojas", + "type": "Veiksmas", + "status": "Statusas", + "details": "Informacija", + "type-added": "Pridėta", + "type-deleted": "Pašalinta", + "type-updated": "Atnaujinta", + "type-attributes-updated": "Atributai atnaujinti", + "type-attributes-deleted": "Atributai pašalinti", + "type-rpc-call": "RPC iškvietimas", + "type-credentials-updated": "Įgaliojimai atnaujinti", + "type-assigned-to-customer": "Priskirta klientui", + "type-unassigned-from-customer": "Atsieta nuo kliento", + "type-assigned-to-edge": "Priskirta Edge įrenginiui", + "type-unassigned-from-edge": "Atsieta nuo Edge įrenginio", + "type-activated": "Aktyvuota", + "type-suspended": "Sustabdyta", + "type-credentials-read": "Įgaliojimų skaitymas", + "type-attributes-read": "Atributų skaitymas", + "type-relation-add-or-update": "Ryšys pridėtas arba atnaujintas", + "type-relation-delete": "Ryšys panaikintas", + "type-relations-delete": "Visi ryšiai panaikinti", + "type-alarm-ack": "Įspėjimas patvirtintas", + "type-alarm-clear": "Įspėjimas išvalytas", + "type-alarm-delete": "Įspėjimas pašalintas", + "type-alarm-assign": "Įspėjimas priskirtas", + "type-alarm-unassign": "Įspėjimas atsietas", + "type-added-comment": "Pridėtas komentaras", + "type-updated-comment": "Atnaujintas komentaras", + "type-deleted-comment": "Pašalintas komentaras", + "type-login": "Prisijungimas", + "type-logout": "Atsijungimas", + "type-lockout": "Užblokuotas", + "status-success": "Sėkminga", + "status-failure": "Nesėkminga", + "audit-log-details": "Audito žurnalo informacija", + "no-audit-logs-prompt": "Audito žurnalo įrašų nėra", + "action-data": "Veiksmų informacija", + "failure-details": "Nesėkmės informacija", + "search": "Žurnalo įrašų paieška", + "clear-search": "Išvalyti paiešką", + "type-assigned-from-tenant": "Priskirta iš nuomotojo", + "type-assigned-to-tenant": "Priskirta nuomotojui", + "type-provision-success": "Įrenginys sėkmingai įtrauktas (provisioned)", + "type-provision-failure": "Įrenginio įtraukimas nepavyko", + "type-timeseries-updated": "Telemetrija atnaujinta", + "type-timeseries-deleted": "Telemetrija pašalinta", + "type-sms-sent": "SMS išsiųsta" + }, + "debug-settings": { + "label": "Derinimo konfigūracija", + "on-failure": "Tik klaidos (24/7)", + "all-messages": "Visos žinutės ({{time}})", + "failures": "Klaidos", + "entity": "objektas", + "hint": { + "main-limited": "Per {{time}} bus įrašyta ne daugiau kaip {{msg}} derinimo žinučių iš {{entity}}.", + "on-failure": "Registruoti tik klaidų žinutes.", + "all-messages": "Registruoti visas derinimo žinutes." + } + }, + "calculated-fields": { + "expression": "Išraiška", + "no-found": "Apskaičiuotų laukų nerasta", + "list": "{ count, plural, =1 {Vienas apskaičiuotas laukas} other {# apskaičiuotų laukų sąrašas} }", + "selected-fields": "{ count, plural, =1 {1 apskaičiuotas laukas} other {# apskaičiuoti laukai} } pasirinkta", + "type": { + "simple": "Paprastas", + "script": "Skriptas" + }, + "arguments": "Argumentai", + "decimals-by-default": "Numatytasis dešimtainių skaičius", + "debugging": "Apskaičiuoto lauko derinimas", + "argument-name": "Argumento pavadinimas", + "datasource": "Duomenų šaltinis", + "add-argument": "Pridėti argumentą", + "test-script-function": "Išbandyti skripto funkciją", + "no-arguments": "Argumentai nesukonfigūruoti", + "argument-settings": "Argumento nustatymai", + "argument-current": "Dabartinis subjektas", + "argument-current-tenant": "Dabartinis valdytojas", + "argument-device": "Įrenginys", + "argument-asset": "Turtas", + "argument-customer": "Klientas", + "argument-tenant": "Dabartinis valdytojas", + "argument-type": "Argumento tipas", + "see-debug-events": "Peržiūrėti derinimo įvykius", + "attribute": "Atributas", + "copy-argument-name": "Kopijuoti argumento pavadinimą", + "timeseries-key": "Laiko sekos raktas", + "device-name": "Įrenginio pavadinimas", + "latest-telemetry": "Naujausia telemetrija", + "rolling": "Laiko sekos kaupimas", + "attribute-scope": "Atributo sritis", + "server-attributes": "Serverio atributai", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendrinami atributai", + "attribute-key": "Atributo raktas", + "default-value": "Numatytoji reikšmė", + "limit": "Maksimalių reikšmių skaičius", + "time-window": "Laiko intervalas", + "customer-name": "Kliento pavadinimas", + "asset-name": "Turto pavadinimas", + "timeseries": "Laiko seka", + "output": "Rezultatas", + "create": "Sukurti naują apskaičiuotą lauką", + "file": "Apskaičiuoto lauko failas", + "invalid-file-error": "Netinkamas failo formatas. Įsitikinkite, kad failas yra tinkamas JSON formatas.", + "import": "Importuoti apskaičiuotą lauką", + "export": "Eksportuoti apskaičiuotą lauką", + "export-failed-error": "Nepavyko eksportuoti apskaičiuoto lauko: {{error}}", + "output-type": "Rezultato tipas", + "delete-title": "Ar tikrai norite pašalinti apskaičiuotą lauką '{{title}}'?", + "delete-text": "Būkite atsargūs — po patvirtinimo apskaičiuotas laukas ir visa susijusi informacija bus negrįžtamai pašalinta.", + "delete-multiple-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 apskaičiuotą lauką} other {# apskaičiuotus laukus} }?", + "delete-multiple-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti apskaičiuoti laukai bus pašalinti kartu su visa susijusia informacija.", + "test-with-this-message": "Išbandyti su šia žinute", + "use-latest-timestamp": "Naudoti naujausią laiko žymą", + "hint": { + "arguments-simple-with-rolling": "Paprasto tipo apskaičiuotas laukas neturėtų turėti laiko sekos kaupimo tipo raktų.", + "arguments-empty": "Argumentai negali būti tušti.", + "expression-required": "Reikalinga išraiška.", + "expression-invalid": "Išraiška netinkama.", + "expression-max-length": "Išraiškos ilgis turi būti trumpesnis nei 255 simboliai.", + "argument-name-required": "Argumento pavadinimas yra būtinas.", + "argument-name-pattern": "Argumento pavadinimas netinkamas.", + "argument-name-duplicate": "Toks argumento pavadinimas jau egzistuoja.", + "argument-name-max-length": "Argumento pavadinimas turi būti trumpesnis nei 256 simboliai.", + "argument-name-forbidden": "Argumento pavadinimas yra rezervuotas ir negali būti naudojamas.", + "argument-type-required": "Reikalingas argumento tipas.", + "max-args": "Pasiektas didžiausias leidžiamų argumentų skaičius.", + "decimals-range": "Numatytasis dešimtainių skaičius turi būti tarp 0 ir 15.", + "expression": "Numatytoji išraiška parodo, kaip paversti temperatūrą iš Farenheito į Celsijų.", + "arguments-entity-not-found": "Argumento paskirties subjektas nerastas.", + "use-latest-timestamp": "Jei įjungta, apskaičiuota reikšmė bus išsaugota naudojant naujausią argumentų telemetrijos laiko žymą, o ne serverio laiką." + } + }, + "ai-models": { + "ai-models": "AI modeliai", + "ai-model": "AI modelis", + "model": "Modelis", + "name": "Pavadinimas", + "ai-provider": "AI tiekėjas", + "no-found": "AI modelių nerasta", + "list": "{ count, plural, =1 {Vienas modelis} other {# modelių sąrašas} }", + "selected-fields": "{ count, plural, =1 {1 modelis} other {# modeliai} } pasirinkta", + "add": "Pridėti modelį", + "delete-model-title": "Ar tikrai norite pašalinti modelį '{{modelName}}'?", + "delete-model-text": "Būkite atsargūs — po patvirtinimo modelis ir visa su juo susijusi informacija bus negrįžtamai pašalinta.", + "delete-models-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 modelį} other {# modelius} }?", + "delete-models-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti modeliai bus pašalinti kartu su visa susijusia informacija.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub Modeliai" + }, + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti ne ilgesnis nei 255 simboliai.", + "provider": "Tiekėjas", + "api-key": "API raktas", + "api-key-required": "API raktas yra privalomas.", + "project-id": "Projekto ID", + "project-id-required": "Projekto ID yra privalomas.", + "location": "Vieta", + "location-required": "Vieta yra privaloma.", + "service-account-key-file": "Paslaugos paskyros rakto failas", + "service-account-key-file-required": "Paslaugos paskyros rakto failas yra privalomas.", + "no-file": "Failas nepasirinktas.", + "drop-file": "Vilkite failą arba spustelėkite, kad pasirinktumėte įkelti failą.", + "personal-access-token": "Asmeninis prieigos raktas", + "personal-access-token-required": "Asmeninis prieigos raktas yra privalomas.", + "configuration": "Konfigūracija", + "model-id": "Modelio ID", + "model-id-required": "Modelio ID yra privalomas.", + "deployment-name": "Diegimo pavadinimas", + "deployment-name-required": "Diegimo pavadinimas yra privalomas.", + "set": "Nustatyti", + "region": "Regionas", + "region-required": "Regionas yra privalomas.", + "access-key-id": "Prieigos rakto ID", + "access-key-id-required": "Prieigos rakto ID yra privalomas.", + "secret-access-key": "Slaptas prieigos raktas", + "secret-access-key-required": "Slaptas prieigos raktas yra privalomas.", + "temperature": "Temperatūra", + "temperature-hint": "Nustato modelio atsakymo atsitiktinumo lygį. Didesnės reikšmės didina atsitiktinumą, mažesnės – mažina.", + "temperature-min": "Turi būti 0 arba daugiau.", + "top-p": "Top P", + "top-p-hint": "Sukuria baseiną tikėtiniausių žetonų, iš kurių modelis gali rinktis. Didesnės reikšmės sukuria didesnį ir įvairesnį baseiną, mažesnės – siauresnį.", + "top-p-min-max": "Turi būti didesnis nei 0 ir neviršyti 1.", + "top-k": "Top K", + "top-k-hint": "Apriboja modelio pasirinkimus iki fiksuoto „K“ tikėtiniausių žetonų rinkinio.", + "top-k-min": "Turi būti 0 arba daugiau.", + "presence-penalty": "Buvo pasirodymo bauda", + "presence-penalty-hint": "Taikoma fiksuota bauda žetonui, jei jis jau buvo panaudotas tekste.", + "frequency-penalty": "Dažnio bauda", + "frequency-penalty-hint": "Taikoma bauda žetono tikimybei, kuri didėja priklausomai nuo jo pasikartojimo dažnio tekste.", + "max-output-tokens": "Maksimalus išvesties žetonų skaičius", + "max-output-tokens-hint": "Nustato didžiausią žetonų skaičių, kurį modelis gali sugeneruoti viename atsakyme.", + "endpoint": "Pabaigos taškas (Endpoint)", + "endpoint-required": "Pabaigos taškas yra privalomas.", + "service-version": "Paslaugos versija", + "check-connectivity": "Tikrinti ryšį", + "check-connectivity-success": "Bandomasis užklausimas sėkmingas", + "check-connectivity-failed": "Bandomasis užklausimas nepavyko", + "no-model-matching": "Modelių, atitinkančių '{{entity}}', nerasta.", + "model-required": "Modelis yra privalomas.", + "no-model-text": "Modelių nerasta." + }, + "confirm-on-exit": { + "message": "Turite neišsaugotų pakeitimų. Ar tikrai norite palikti šį puslapį?", + "html-message": "Turite neišsaugotų pakeitimų.
    Ar tikrai norite palikti šį puslapį?", + "title": "Neišsaugoti pakeitimai" + }, + "contact": { + "country": "Šalis", + "country-required": "Country is required.", + "city": "Miestas", + "state": "Rajonas", + "postal-code": "Pašto kodas", + "postal-code-invalid": "Neteisingas pašto kodo formatas.", + "address": "Adresas", + "address2": "Adresas 2", + "phone": "Telefonas", + "email": "El. paštas", + "no-address": "Adresas nenurodytas", + "no-country-found": "No countries found.", + "no-country-matching": "No country matching '{{country}}' were found.", + "state-max-length": "Rajono pavadinimas negali viršyti 256 simbolių", + "phone-max-length": "Telefono numeris negali viršyti 256 simbolių", + "city-max-length": "Miesto pavadinimas negali viršyti 256 simbolių" + }, + "common": { + "name": "Pavadinimas", + "type": "Tipas", + "general": "Bendra informacija", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "data": "Duomenys", + "timestamp": "Laiko žyma", + "enter-username": "Įveskite vartotojo vardą", + "enter-password": "Įveskite slaptažodį", + "enter-search": "Įveskite paieškos kriterijus", + "created-time": "Sukūrimo laikas", + "disabled": "Išjungta", + "loading": "Įkeliama...", + "proceed": "Tęsti", + "open-details-page": "Atidaryti informacijos puslapį", + "not-found": "Nerasta", + "value": "Reikšmė", + "documentation": "Dokumentacija", + "time-left": "Likęs laikas: {{time}}", + "output": "Išvestis", + "suffix": { + "s": "s", + "ms": "ms" + }, + "hint": { + "name-required": "Pavadinimas yra privalomas.", + "name-pattern": "Pavadinimas netinkamas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "title-required": "Antraštė yra privaloma.", + "title-pattern": "Antraštė netinkama.", + "title-max-length": "Antraštė turi būti trumpesnė nei 256 simboliai.", + "key-required": "Raktas yra privalomas.", + "key-pattern": "Raktas netinkamas.", + "key-max-length": "Raktas turi būti trumpesnis nei 256 simboliai." + }, + "required-fields": "Trūksta privalomų laukų" + }, + "content-type": { + "json": "Json", + "text": "Tekstas", + "binary": "Binarinis (Base64)" + }, + "color": { + "color": "Spalva" + }, + "customer": { + "customer": "Klientas", + "customers": "Klientai", + "management": "Klientų valdymas", + "dashboard": "Kliento skydelis", + "dashboards": "Klientų skydeliai", + "devices": "Kliento įrenginiai", + "entity-views": "Kliento subjektų rodiniai", + "assets": "Kliento turtas", + "public-dashboards": "Vieši skydeliai", + "public-devices": "Vieši įrenginiai", + "public-assets": "Viešas turtas", + "public-entity-views": "Vieši subjektų rodiniai", + "add": "Pridėti klientą", + "delete": "Pašalinti klientą", + "manage-customer-users": "Kliento vartotojai", + "manage-customer-devices": "Kliento įrenginiai", + "manage-customer-dashboards": "Kliento skydeliai", + "manage-public-devices": "Vieši įrenginiai", + "manage-public-dashboards": "Vieši skydeliai", + "manage-customer-assets": "Kliento turtas", + "manage-customer-edges": "Kliento Edge įrenginiai", + "manage-public-assets": "Viešas turtas", + "add-customer-text": "Pridėti naują klientą", + "no-customers-text": "Klientų nėra", + "customer-details": "Informacija apie klientą", + "delete-customer-title": "Ar tikrai norite pašalinti klientą '{{customerTitle}}'?", + "delete-customer-text": "Būkite dėmesingi – po patvirtinimo kliento ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-customers-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 klientą} other {# klientus} }?", + "delete-customers-action-title": "Pašalinti { count, plural, =1 {1 klientą} other {# klientus} }", + "delete-customers-text": "Būkite dėmesingi – po patvirtinimo klientų ir su jais susijusios informacijos atkurti nebegalėsite.", + "manage-users": "Vartotojai", + "manage-assets": "Turtas", + "manage-devices": "Įrenginiai", + "manage-dashboards": "Skydeliai", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti kliento ID", + "idCopiedMessage": "Kliento ID nukopijuotas į iškarpinę", + "select-customer": "Pasirinkti klientą", + "no-customers-matching": "Klientų, atitinkančių '{{entity}}', nerasta.", + "customer-required": "Klientas yra privalomas", + "select-default-customer": "Pasirinkite numatytąjį klientą", + "default-customer": "Numatytasis klientas", + "default-customer-required": "Numatytasis klientas būtinas, norint suderinti prietaisų skydelį valdytojo lygiu", + "search": "Klientų paieška", + "selected-customers": "Pasirinkta { count, plural, =1 {1 klientas} other {# klientai} }", + "edges": "Kliento Edge įrenginiai", + "manage-edges": "Valdyti Edge įrenginius" + }, + "css-size": { + "size-value-required": "Dydžio reikšmė yra privaloma", + "invalid-size-value": "Neteisinga dydžio reikšmė" + }, + "date": { + "last-update-n-ago": "Paskutinis atnaujinimas prieš N", + "last-update-n-ago-text": "Paskutinis atnaujinimas prieš {{ agoText }}", + "custom-date": "Pasirinkta data", + "format": "Formatas", + "preview": "Peržiūra", + "auto": "Automatinis", + "time-granularity-formats": "Laiko granuliacijos formatai", + "unit-year": "Metai", + "unit-month": "Mėnesiai", + "unit-day": "Dienos", + "unit-hour": "Valandos", + "unit-minute": "Minutės", + "unit-second": "Sekundės", + "unit-millisecond": "Milisekundės" + }, + "datetime": { + "date-from": "Data nuo", + "time-from": "Laikas nuo", + "date-to": "Data iki", + "time-to": "Laikas iki", + "from": "Nuo", + "to": "Iki" + }, + "dashboard": { + "dashboard": "Skydelis", + "dashboards": "Skydeliai", + "management": "Skydelių valdymas", + "view-dashboards": "Peržiūrėti skydelius", + "add": "Pridėti skydelį", + "assign-dashboard-to-customer": "Skydelį (-ius) priskirti klientui", + "assign-dashboard-to-customer-text": "Pasirinkite skydelius, kuriuos norite priskirti klientui", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti skydelį (-ius)", + "assign-to-customer": "Priskirti klientui", + "unassign-from-customer": "Atsieti nuo kliento", + "make-public": "Skydelį padaryti viešą", + "make-private": "Skydelį padaryti privatų", + "manage-assigned-customers": "Valdyti priskirtus klientus", + "assigned-customers": "Priskirti klientai", + "assign-to-customers": "Skydelį (-ius) priskirti klientams", + "assign-to-customers-text": "Pasirinkite klientus, kuriems priskirti skydelį (-ius)", + "unassign-from-customers": "Skydelį (-ius) atsieti nuo klientų", + "unassign-from-customers-text": "Pasirinkite klientus, kuriems atsieti skydelį (-ius)", + "no-dashboards-text": "Skydelių nėra", + "no-widgets": "Valdikliai nesukonfigūruoti", + "add-widget": "Pridėti naują valdiklį", + "add-widget-button-text": "Pridėti valdiklį", + "title": "Pavadinimas", + "image": "Skydelio paveikslėlis", + "mobile-app-settings": "Mobiliosios programėlės nustatymai", + "mobile-order": "Skydelio eiliškumas mobiliojoje programėlėje", + "mobile-hide": "Skydelį slėpti mobiliojoje programėlėje", + "update-image": "Atnaujinti skydelio paveikslėlį", + "take-screenshot": "Padaryti ekrano nuotrauką", + "select-widget-title": "Pasirinkite valdiklį", + "select-widget-value": "{{title}}: pasirinkite valdiklį", + "select-widget-subtitle": "Galimų valdiklių sąrašas", + "delete": "Pašalinti skydelį", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "details": "Informacija", + "dashboard-details": "Skydelio informacija", + "add-dashboard-text": "Pridėti naują skydelį", + "assign-dashboards": "Priskirti skydelius", + "assign-new-dashboard": "Priskirti naują skydelį", + "assign-dashboards-text": "Priskirti { count, plural, =1 {1 skydelį} other {# skydelius} } klientams", + "unassign-dashboards-action-text": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo klientų", + "delete-dashboards": "Panaikinti skydelius", + "unassign-dashboards": "Atsieti skydelius", + "unassign-dashboards-action-title": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo kliento", + "delete-dashboard-title": "Ar tikrai norite pašalinti skydelį '{{dashboardTitle}}'?", + "delete-dashboard-text": "Būkite dėmesingi, po patvirtinimo skydelio ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-dashboards-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "delete-dashboards-action-title": "Pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }", + "delete-dashboards-text": "Būkite dėmesingi, po patvirtinimo pasirinkti skydeliai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "unassign-dashboard-title": "Ar tikrai norite atsieti skydelį '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Po patvirtinimo skydelis bus atsietas ir klientas jo nebematys.", + "unassign-dashboard": "Atsieti skydelį", + "unassign-dashboards-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "unassign-dashboards-text": "Po patvirtinimo visi skydeliai bus atsieti ir klientas jų nebematys.", + "public-dashboard-title": "Skydelis dabar yra viešas", + "public-dashboard-text": "Skydelis {{dashboardTitle}} dabar yra viešas ir pasiekiamas per šią nuorodą:", + "public-dashboard-notice": "Pastaba: Įrenginiai turi būti vieši, jei norite matyti jų duomenis.", + "make-private-dashboard-title": "Ar tikrai norite skydelį '{{dashboardTitle}}' padaryti privačiu?", + "make-private-dashboard-text": "Po patvirtinimo skydelis taps privačiu ir jo nematys kiti vartotojai.", + "make-private-dashboard": "Skydelį padaryti privačiu", + "socialshare-text": "'{{dashboardTitle}}' sukurta naudojant ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' sukurta naudojant ThingsBoard", + "select-dashboard": "Pasirinkti skydelį", + "no-dashboards-matching": "Skydelio, atitinkančio '{{entity}}', nėra.", + "dashboard-required": "Skydelis būtinas.", + "select-existing": "Pasirinkti esamą skydelį", + "create-new": "Sukurti naują skydelį", + "new-dashboard-title": "Naujo skydelio pavadinimas", + "open-dashboard": "Atidaryti skydelį", + "set-background": "Nustatyti foną", + "background-color": "Fono spalva", + "background-image": "Fono paveikslėlis", + "background-size-mode": "Fono dydis", + "no-image": "Paveikslėlis nepasirinktas", + "empty-image": "Nėra paveikslėlio", + "drop-image": "Užvilkite paveikslėlį arba spustelėkite ir pasirinkite failą.", + "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", + "cannot-upload-file": "Failo įkelti nepavyko", + "settings": "Nustatymai", + "move-all-widgets": "Perkelti visus valdiklius", + "move-by": "Perkelti per", + "cols": "Stulpeliai", + "rows": "Eilutės", + "layout": "Išdėstymas", + "layout-type-default": "Numatytasis", + "layout-type-scada": "SCADA", + "layout-type-divider": "Skyriklis", + "layout-settings-type": "Išdėstymo nustatymai: {{ type }} lūžio taškas", + "columns-count": "Stulpelių skaičius", + "columns-count-required": "Stulpelių skaičius būtinas.", + "min-columns-count-message": "Minimalus stulpelių skaičius - 10.", + "max-columns-count-message": "Maksimalus stulpelių skaičius - 1000.", + "min-layout-width": "Minimalus išdėstymo plotis", + "columns-suffix": "stulpeliai", + "widgets-margins": "Paraštės tarp valdiklių dydis", + "margin-required": "Paraštės dydžio reikšmė būtina.", + "min-margin-message": "Minimali paraštės dydžio reikšmė - 0.", + "max-margin-message": "Maksimali paraštės dydžio reikšmė - 50.", + "horizontal-margin": "Horizontalios paraštės dydis", + "horizontal-margin-required": "Horizontalios paraštės dydžio reikšmė būtina.", + "min-horizontal-margin-message": "Minimali horizontalios paraštės dydžio reikšmė - 0", + "max-horizontal-margin-message": "Maksimali horizontalios paraštės dydžio reikšmė - 50.", + "vertical-margin": "Vertikalios paraštės dydis", + "vertical-margin-required": "Vertikalios paraštės dydžio reikšmė būtina.", + "min-vertical-margin-message": "Minimali vertikalios paraštės dydžio reikšmė - 0.", + "max-vertical-margin-message": "Maksimali vertikalios paraštės dydžio reikšmė - 50.", + "apply-outer-margin": "Taikyti paraštes maketo šonuose", + "autofill-height": "Automatiškai pritaikyti aukštį", + "mobile-layout": "Mobiliojo režimo išdėstymo nustatymai", + "mobile-row-height": "Mobiliojo režimo eilutės aukštis pikseliais", + "mobile-row-height-required": "Mobiliojo režimo eilutės aukštis būtinas.", + "min-mobile-row-height-message": "Minimalus mobiliojo režimo eilutės aukštis yra 5 pikseliai.", + "max-mobile-row-height-message": "Maksimalus mobiliojo režimo eilutės aukštis yra 200 pikselių.", + "row-height": "Eilutės aukštis", + "row-height-required": "Eilutės aukščio reikšmė yra privaloma.", + "min-row-height-message": "Mažiausias eilutės aukštis – 5 pikseliai.", + "max-row-height-message": "Didžiausias eilutės aukštis – 200 pikselių.", + "display-first-in-mobile-view": "Rodyti pirmą mobiliajame rodinyje", + "title-settings": "Pavadinimo nustatymai", + "display-title": "Rodyti skydelio pavadinimą", + "title-color": "Pavadinimo spalva", + "toolbar-settings": "Įrankių juostos nustatymai", + "hide-toolbar": "Paslėpti įrankių juostą", + "toolbar-always-open": "Įrankių juostą laikyti atidarytą", + "display-dashboards-selection": "Rodyti skydelių pasirinkimą", + "display-entities-selection": "Rodyti subjektų pasirinkimą", + "display-filters": "Rodyti filtrus", + "display-dashboard-timewindow": "Rodyti laiko langą", + "display-dashboard-export": "Rodyti eksportą", + "display-update-dashboard-image": "Rodyti skydelio paveikslėlio atnaujinimą", + "dashboard-logo-settings": "Skydelio logotipo nustatymai", + "display-dashboard-logo": "Rodyti logotipą skydelio viso ekrano režime", + "dashboard-logo-image": "Skydelio logotipo paveikslėlis", + "advanced-settings": "Išplėstiniai nustatymai", + "dashboard-css": "Skydelio CSS", + "import": "Importuoti skydelį", + "export": "Eksportuoti skydelį", + "export-failed-error": "Skydelio eksportuoti nepavyko: {{error}}", + "export-prompt": "Įterpti skydelio paveikslėlius ir išteklius", + "create-new-dashboard": "Sukurti naują skydelį", + "dashboard-file": "Skydelio failas", + "invalid-dashboard-file-error": "Skydelio importuoti nepavyko: neteisinga duomenų struktūra.", + "dashboard-import-missing-aliases-title": "Konfigūruoti importuoto skydelio naudojamus pseudonimus", + "create-new-widget": "Sukurti naują valdiklį", + "import-widget": "Importuoti valdiklį", + "widget-file": "Valdiklio failas", + "invalid-widget-file-error": "Valdiklio importuoti nepavyko: neteisinga duomenų struktūra.", + "widget-import-missing-aliases-title": "Konfigūruoti importuoto valdiklio naudojamus pseudonimus", + "open-toolbar": "Atidaryti skydelio įrankių juostą", + "close-toolbar": "Uždaryti įrankių juostą", + "configuration-error": "Konfigūracijos klaida", + "alias-resolution-error-title": "Skydelio pseudonimų konfigūracijos klaida", + "invalid-aliases-config": "Įrenginių, atitinkančių pseudonimų filtrus, nėra.
    Susisiekite su sistemos administratoriumi.", + "select-devices": "Pasirinkite įrenginius", + "assignedToCustomer": "Priskirta klientui", + "assignedToCustomers": "Priskirta klientams", + "public": "Viešas", + "copyId": "Kopijuoti skydelio Id", + "idCopiedMessage": "Skydelio Id nukopijuotas į iškarpinę", + "public-link": "Vieša nuoroda", + "copy-public-link": "Kopijuoti viešą nuorodą", + "public-link-copied-message": "Skydelio vieša nuoroda nukopijuota į iškarpinę", + "manage-states": "Valdyti skydelio būsenas", + "states": "Skydelio būsenos", + "states-short": "Būsenos", + "search-states": "Skydelio būsenų paieška", + "selected-states": "Pasirinkta skydelio { count, plural, =1 {1 būsena} other {# būsenos} }", + "edit-state": "Redaguoti skydelio būseną", + "delete-state": "Panaikinti skydelio būseną", + "add-state": "Pridėti skydelio būseną", + "no-states-text": "Būsenų nėra", + "state": "Skydelio būsena", + "state-name": "Pavadinimas", + "state-name-required": "Skydelio pavadinimas būtinas.", + "state-id": "Būsenos Id", + "state-id-required": "Būsenos Id būtinas.", + "state-id-exists": "Skydelio būsena su tokiu pačiu Id jau yra.", + "is-root-state": "Pagrindinė būsena", + "delete-state-title": "Panaikinti skydelio būseną", + "delete-state-text": "Ar tikrai norite panaikinti skydelio būseną '{{stateName}}'?", + "show-details": "Rodyti detales", + "hide-details": "Slėpti detales", + "select-state": "Pasirinkite būseną", + "state-controller": "Būsenų valdiklis", + "state-controller-default": "static (pasenęs)", + "search": "Skydelių paieška", + "selected-dashboards": "Pasirinkta { count, plural, =1 {1 skydelis} other {# skydeliai} }", + "home-dashboard": "Pagrindinis skydelis", + "home-dashboard-hide-toolbar": "Slėpti pagrindinio skydelio įrankių juostą", + "unassign-dashboard-from-edge-text": "Po patvirtinimo skydelis bus atsietas ir nebus pasiekiamas Edge įrenginio.", + "unassign-dashboards-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "unassign-dashboards-from-edge-text": "Po patvirtinimo visi pasirinkti skydeliai bus atsieti ir nebus pasiekiami Edge įrenginio.", + "assign-dashboard-to-edge": "Priskirti skydelį (-ius) Edge įrenginiui", + "assign-dashboard-to-edge-text": "Pasirinkite skydelius, kuriuos priskirti Edge įrenginiui", + "non-existent-dashboard-state-error": "Skydelio būsena su id „{{ stateId }}“ nerasta", + "edit-mode": "Redagavimo režimas", + "duplicate-state-action": "Dubliuoti būseną", + "breakpoint-value": "Lūžio taškas ({{ value }})", + "breakpoints-id": { + "default": "Numatytasis", + "xs": "Mobilusis (xs)", + "sm": "Planšetė (sm)", + "md": "Nešiojamas (md)", + "lg": "Stalinis (lg)", + "xl": "Stalinis (xl)" + }, + "view-format-type-grid": "Tinklelis", + "view-format-type-list": "Sąrašas", + "view-format": "Rodinio formatas" + }, + "datakey": { + "settings": "Nustatymai", + "general": "Bendra", + "advanced": "Pažangūs nustatymai", + "key": "Raktas", + "keys": "Raktai", + "label": "Etiketė", + "color": "Spalva", + "units": "Vienetai (simboliai, rodomi šalia reikšmės)", + "decimals": "Skaitmenų po kablelio kiekis", + "data-generation-func": "Duomenų generavimo funkcija", + "use-data-post-processing-func": "Naudoti papildomo duomenų apdorojimo funkciją", + "configuration": "Duomenų raktų konfigūracija", + "timeseries": "Telemetrija", + "attributes": "Atributai", + "entity-field": "Subjekto laukas", + "alarm": "Įspėjimo laukai", + "timeseries-required": "Subjekto telemetrija būtina.", + "timeseries-or-attributes-required": "Subjekto telemetrija arba atributai būtini.", + "alarm-fields-timeseries-or-attributes-required": "Įspėjimo laukai arba subjekto telemetrija/atributai būtini.", + "maximum-timeseries-or-attributes": "Leidžiama daugiausiai { count, plural, =1 {1 telemetrija/atributas} other {# telemetrijų/atributų} }.", + "alarm-fields-required": "Įspėjimo laukai būtini.", + "function-types": "Funkcijų tipai", + "function-type": "Funkcijos tipas", + "function-types-required": "Funkcijos tipas būtinas.", + "data-keys": "Duomenų raktai", + "data-key": "Duomenų raktas", + "data-keys-required": "Duomenų raktai būtini.", + "data-key-required": "Duomenų raktas būtinas.", + "alarm-keys": "Įspėjimo duomenų raktai", + "alarm-key": "Įspėjimo duomenų raktas", + "alarm-key-functions": "Įspėjimo rakto funkcijos", + "alarm-key-function": "Įspėjimo rakto funkcija", + "latest-keys": "Naujausi duomenų raktai", + "latest-key": "Naujausias duomenų raktas", + "latest-key-functions": "Naujausio rakto funkcijos", + "latest-key-function": "Naujausio rakto funkcija", + "timeseries-keys": "Telemetrijos duomenų raktai", + "timeseries-key": "Telemetrijos duomenų raktas", + "timeseries-key-functions": "Telemetrijos rakto funkcijos", + "timeseries-key-function": "Telemetrijos rakto funkcija", + "maximum-function-types": "Daugiausia leidžiama { count, plural, =1 {1 funkcijos tipas} other {# funkcijos tipai} }.", + "time-description": "Dabartinės vertės laiko žyma;", + "value-description": "Dabartinė vertė;", + "prev-value-description": "Ankstesnio funkcijos kvietimo rezultatas;", + "time-prev-description": "Ankstesnės vertės laiko žyma;", + "prev-orig-value-description": "Pradinė ankstesnė vertė;", + "aggregation": "Agregavimas", + "aggregation-type-hint-common": "Dėl našumo priežasčių agreguotų reikšmių skaičiavimas galimas tik fiksuotiems laiko intervalams, pvz., 'dabartinė diena', 'dabartinis mėnuo' ir pan. Tai netaikoma slankiesiems intervalams, kaip 'paskutinės 30 min' ar 'paskutinės 24 val'.", + "aggregation-type-none-hint": "Naudoti naujausią reikšmę.", + "aggregation-type-min-hint": "Rasti mažiausią reikšmę tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-max-hint": "Rasti didžiausią reikšmę tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-avg-hint": "Apskaičiuoti vidurkį tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-sum-hint": "Suskaičiuoti visų duomenų taškų reikšmių sumą pasirinktoje laiko atkarpoje.", + "aggregation-type-count-hint": "Duomenų taškų skaičius pasirinktoje laiko atkarpoje.", + "delta-calculation": "Delta skaičiavimas", + "enable-delta-calculation": "Įjungti delta skaičiavimą", + "enable-delta-calculation-hint": "Kai įjungta, rakto reikšmė skaičiuojama pagal agreguotas reikšmes pasirinktam laikotarpiui ir lyginimo periodui. Dėl našumo delta skaičiavimas galimas tik istoriniams laikotarpiams, o ne realaus laiko reikšmėms. Pvz., galima apskaičiuoti skirtumą tarp vakarykščio ir užvakar dienos energijos suvartojimo.", + "delta-calculation-result": "Delta skaičiavimo rezultatas", + "delta-calculation-result-previous-value": "Ankstesnė reikšmė", + "delta-calculation-result-delta-absolute": "Delta (absoliuti)", + "delta-calculation-result-delta-percent": "Delta (procentinė)", + "source": "Šaltinis", + "latest": "Naujausia", + "latest-value": "Naujausia reikšmė", + "delta": "delta", + "percent": "procentai", + "absolute": "absoliuti" + }, + "datasource": { + "type": "Duomenų šaltinio tipas", + "name": "Pavadinimas", + "label": "Etiketė", + "add-datasource-prompt": "Pridėkite duomenų šaltinį" + }, + "details": { + "details": "Informacija", + "edit-mode": "Redagavimo režimas", + "edit-json": "Redaguoti JSON", + "toggle-edit-mode": "Redagavimo režimas" + }, + "device": { + "device": "Įrenginys", + "device-required": "Įrenginys būtinas.", + "devices": "Įrenginiai", + "management": "Įrenginių valdymas", + "view-devices": "Peržiūrėti įrenginius", + "device-alias": "Įrenginio pseudonimas", + "device-type-max-length": "Įrenginio tipas negali viršyti 256 simbolių", + "aliases": "Įrenginių pseudonimai aliases", + "no-alias-matching": "'{{alias}}' nerasta.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerasta.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Sukonfigūruoti '{{alias}}' pseudonimą", + "no-devices-matching": "Įrenginių, attinkančių '{{entity}}' nėra.", + "alias": "Pseudonimas", + "alias-required": "Įrenginio pseudonimas būtinas.", + "remove-alias": "Panaikinti įrenginio pseudonimą", + "add-alias": "Pridėti įrenginio pseudonimą", + "name-starts-with": "Įrenginio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%įrenginio_pavadinimo_fragmentas%', '%įrenginio_pavadinimo_pabaiga', 'įrenginio_pavadinimo_pradžia%'.", + "device-list": "Įrenginių sąrašas", + "use-device-name-filter": "Naudoti filtrą", + "device-list-empty": "Įrenginiai nepasirinkti.", + "device-name-filter-required": "Įrenginio pavadinimo filtras turi būti nustatytas.", + "device-name-filter-no-device-matched": "Įrenginių, kurių pavadinimas prasideda '{{device}}' nėra.", + "add": "Pridėti įrenginį", + "assign-to-customer": "Priskirti kientui", + "assign-device-to-customer": "Įrenginį (-ius) priskirti klientui", + "assign-device-to-customer-text": "Pasirinkite įrenginius, kuriuos norite priskirti klientui", + "make-public": "Įrenginį padaryti viešu", + "make-private": "Įrenginį padaryti privačiu", + "no-devices-text": "Įrenginių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti įrenginį (-ius)", + "device-details": "Įrenginio informacija", + "add-device-text": "Pridėti naują įrenginį", + "credentials": "Įgaliojimai", + "manage-credentials": "Valdyti įgaliojimus", + "delete": "Panaikinti įrenginį", + "assign-devices": "priskirti įrenginius", + "assign-devices-text": "Priskirti { count, plural, =1 {1 įrenginį} other {# įrenginius} } klientui", + "delete-devices": "Panaikinti įrenginius", + "unassign-from-customer": "Atseti nuo kliento", + "unassign-devices": "Atsieti įrenginius", + "unassign-devices-action-title": "Atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo kliento", + "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", + "unassign-devices-from-edge": "Unassign devices from edge", + "assign-new-device": "Priskirti naują įrenginį", + "make-public-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti viešu?", + "make-public-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys bus vieši ir prieinami kitiems vartotojams.", + "make-private-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti privačiu?", + "make-private-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys taps privatūs ir nebus prieinami kitiems vartotojams.", + "view-credentials": "Peržiūrėti įgaliojimus", + "delete-device-title": "Ar tikrai norite panaikinti įrenginį '{{deviceName}}'?", + "delete-device-text": "Būkite dėmesingi, po patvirtinimo įrenginys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite", + "delete-devices-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "delete-devices-action-title": "Panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }", + "delete-devices-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti įrenginiai ir su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "unassign-device-title": "Ar tikrai norite atsieti įrenginį '{{deviceName}}'?", + "unassign-device-text": "Po patvirtinimo įrenginys bus atsietas ir klientas jo nebematys.", + "unassign-device": "Atsieti įrenginį", + "unassign-devices-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "unassign-devices-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti nuo kliento.", + "device-credentials": "įrenginio įgaliojimai", + "loading-device-credentials": "Įkeliami įrenginio įgaliojimai...", + "credentials-type": "Įgaliojimų tipas", + "access-token": "Prieigos raktas", + "access-token-required": "Prieigos raktas būtinas.", + "access-token-invalid": "Prieigos rakto ilgis turi būti nuo 1 iki 32 simbolių.", + "certificate-pem-format": "Sertifikatas PEM formate", + "certificate-pem-format-required": "Certificate is required.", + "copy-access-token": "Kopijuoti prieigos raktą", + "copy-certificate": "Kopijuoti sertifikatą", + "copy-client-id": "Kopijuoti kliento ID", + "copy-user-name": "Kopijuoti vartotojo vardą", + "copy-password": "Kopijuoti slaptažodį", + "generate-client-id": "Generuoti kliento ID", + "generate-user-name": "Generuoti vartotojo vardą", + "generate-password": "Generuoti slaptažodį", + "generate-access-token": "Generuoti prieigos raktą", + "lwm2m-security-config": { + "identity": "Kliento identifikatorius", + "identity-required": "Kliento identifikatorius būtinas.", + "identity-tooltip": "PSK identifikatorius yra savavališkai parinktas iki 128 baitų identifikatorius, kaip aprašyta standarte [RFC7925].\nPSK identifikatorius pirmiausia turi būti konvertuotas į simbolių eilutę ir tada užkoduotas į baitus naudojant UTF-8.", + "client-key": "Kliento raktas", + "client-key-required": "Kliento raktas būtinas.", + "client-key-tooltip-prk": "RPK viešasis raktas arba ID turi atitikti standartą [RFC7250] ir būti užkoduotas Base64 formatu!", + "client-key-tooltip-psk": "PSK raktas turi atitikti standartą [RFC4279] ir būti HexDec formatu: 32, 64 arba 128 simboliai!", + "endpoint": "Kliento galinio taško pavadinimas (Endpoint)", + "endpoint-required": "Kliento galinio taško pavadinimas būtinas.", + "client-public-key": "Kliento viešasis raktas", + "client-public-key-hint": "Jei viešasis raktas nenurodytas, bus naudojamas patikimas sertifikatas.", + "client-public-key-tooltip": "X509 viešasis raktas turi būti DER koduotas X509v3 formatu, naudoti tik EC algoritmą ir būti užkoduotas Base64 formatu!", + "mode": "Saugumo konfigūracijos režimas", + "client-tab": "Kliento saugumo konfigūracija", + "client-certificate": "Kliento sertifikatas", + "bootstrap-tab": "Bootstrap klientas", + "bootstrap-server": "Bootstrap serveris", + "lwm2m-server": "LwM2M serveris", + "client-publicKey-or-id": "Kliento viešasis raktas arba ID", + "client-publicKey-or-id-required": "Kliento viešasis raktas arba ID būtinas.", + "client-publicKey-or-id-tooltip-psk": "PSK identifikatorius yra savavališkai parinktas iki 128 baitų identifikatorius, kaip aprašyta standarte [RFC7925].\nPSK identifikatorius pirmiausia turi būti konvertuotas į simbolių eilutę ir tada užkoduotas į baitus naudojant UTF-8.", + "client-publicKey-or-id-tooltip-rpk": "RPK viešasis raktas arba ID turi atitikti standartą [RFC7250] ir būti užkoduotas Base64 formatu!", + "client-publicKey-or-id-tooltip-x509": "X509 viešasis raktas turi būti DER koduotas X509v3 formatu, naudoti tik EC algoritmą ir būti užkoduotas Base64 formatu!", + "client-secret-key": "Kliento slaptasis raktas", + "client-secret-key-required": "Kliento slaptasis raktas būtinas.", + "client-secret-key-tooltip-psk": "PSK raktas turi atitikti standartą [RFC4279] ir būti HexDec formatu: 32, 64 arba 128 simboliai!", + "client-secret-key-tooltip-prk": "RPK slaptasis raktas turi būti PKCS_8 formatu (DER kodavimas, standartas [RFC5958]) ir būti užkoduotas Base64 formatu!", + "client-secret-key-tooltip-x509": "X509 slaptasis raktas turi būti PKCS_8 formatu (DER kodavimas, standartas [RFC5958]) ir būti užkoduotas Base64 formatu!" + }, + "client-id": "Kliento ID", + "client-id-pattern": "Yra netinkamas simbolis.", + "user-name": "Vartotojo vardas", + "user-name-required": "Vartotojo vardas būtinas.", + "client-id-or-user-name-necessary": "Kliento ID ir/arba vartotojo vardas būtini.", + "password": "Slaptažodis", + "secret": "Slaptas raktas", + "secret-required": "Slaptas raktas būtinas.", + "device-type": "Įrenginio tipas", + "device-type-required": "Įrenginio tipas būtinas.", + "select-device-type": "Pasirinkite įrenginio tipą", + "enter-device-type": "Įveskite įrenginio tipą", + "any-device": "Bet kuris įrenginys", + "no-device-types-matching": "Įrenginio tipų, atitinkančių '{{entitySubtype}}', nėra.", + "device-type-list-empty": "Nepasirinktas įrenginio tipas!", + "device-profile-type-list-empty": "Turi būti pasirinktas bent vienas įrenginio profilis.", + "device-types": "Įrenginių tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "label-max-length": "Etiketė negali viršyti 256 simbolių.", + "description": "Aprašymas", + "label": "Etiketė", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti įrenginio ID", + "copyAccessToken": "Kopijuoti prieigos raktą", + "copy-mqtt-authentication": "Kopijuoti MQTT autentifikacijos duomenis", + "idCopiedMessage": "Įrenginio ID nukopijuotas į iškarpinę.", + "accessTokenCopiedMessage": "Įrenginio prieigos raktas nukopijuotas į iškarpinę.", + "mqtt-authentication-copied-message": "Įrenginio MQTT autentifikacijos duomenys nukopijuoti į iškarpinę.", + "assignedToCustomer": "Priskirtas klientui", + "unable-delete-device-alias-title": "Nepavyko panaikinti įrenginio pseudonimo", + "unable-delete-device-alias-text": "Įrenginio pseudonimo '{{deviceAlias}}' panaikinti nepavyko, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "is-gateway": "Yra šliuzas (Gateway)", + "overwrite-activity-time": "Perrašyti aktyvumo laiką prijungtam įrenginiui", + "device-filter": "Įrenginių filtras", + "device-filter-title": "Įrenginių filtras", + "filter-title": "Filtras", + "device-state": "Įrenginio būsena", + "state": "Būsena", + "any": "Visos", + "active": "Aktyvus", + "inactive": "Neaktyvus", + "public": "Viešas", + "device-public": "Įrenginys yra viešas", + "select-device": "Pasirinkite įrenginį", + "import": "Importuoti įrenginį", + "device-file": "Įrenginio failas", + "search": "Įrenginių paieška", + "selected-devices": "Pasirinkta { count, plural, =1 {1 įrenginys} other {# įrenginiai} }", + "device-configuration": "Įrenginio konfigūracija", + "transport-configuration": "Transporto konfigūracija", + "wizard": { + "device-details": "Informacija apie įrenginį" + }, + "unassign-devices-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo Edge?", + "unassign-devices-from-edge-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti ir nebebus pasiekiami per Edge.", + "time": "Laikas", + "connectivity": { + "check-connectivity": "Patikrinti ryšį", + "device-created-check-connectivity": "Įrenginys sukurtas. Patikrinkime ryšį!", + "loading-check-connectivity-command": "Įkeliamos ryšio tikrinimo komandos...", + "use-following-instructions": "Naudokite šias instrukcijas, kad išsiųstumėte telemetriją įrenginio vardu naudodami komandų eilutę (shell).", + "execute-following-command": "Vykdykite šią komandą", + "install-curl-windows": "Nuo Windows 10 versijos 17063, cURL įrankis prieinamas pagal numatymą.", + "install-curl-macos": "Nuo Mac OS X 10.2 (Jaguar) versijos, cURL įrankis prieinamas pagal numatymą.", + "install-mqtt-windows": "Naudokite instrukcijas, kad atsisiųstumėte, įdiegtumėte, sukonfigūruotumėte ir paleistumėte „mosquitto_pub“ įrankį.", + "install-coap-client": "Naudokite instrukcijas, kad atsisiųstumėte, įdiegtumėte, sukonfigūruotumėte ir paleistumėte „coap-client“ įrankį.", + "install-necessary-client-tools": "Įdiekite būtinus kliento įrankius", + "mqtts-x509-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per MQTT su X.509 autorizacija.", + "coaps-x509-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per CoAP (DTLS) su X.509 autorizacija.", + "snmp-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per SNMP protokolą.", + "sparkplug-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per MQTT Sparkplug protokolą.", + "lwm2m-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per LwM2M protokolą." + } + }, + "dynamic-form": { + "property": { + "properties": "Savybės", + "property": "Savybė", + "id": "Id", + "name": "Pavadinimas", + "type": "Tipas", + "type-text": "Tekstas", + "type-password": "Slaptažodis", + "type-textarea": "Teksto sritis", + "type-number": "Skaičius", + "type-switch": "Perjungiklis", + "type-select": "Pasirinkimas", + "type-radios": "Žymimieji mygtukai", + "type-datetime": "Data / Laikas", + "type-image": "Paveikslėlis", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "Spalva", + "type-color-settings": "Spalvų nustatymai", + "type-font": "Šriftas", + "type-units": "Vienetai", + "type-icon": "Piktograma", + "type-fieldset": "Lauko rinkinys (fieldset)", + "type-array": "Masyvas", + "type-html-section": "HTML sekcija", + "group-title": "Grupės pavadinimas", + "no-properties": "Savybės nesukonfigūruotos", + "add-property": "Pridėti savybę", + "property-settings": "Savybės nustatymai", + "remove-property": "Pašalinti savybę", + "default-value": "Numatytoji reikšmė", + "value-required": "Reikšmė būtina", + "number-settings": "Skaičiaus nustatymai", + "min": "Min", + "max": "Maks", + "step": "Žingsnis", + "selected-options-limit": "Pasirinktų parinkčių limitas", + "advanced-ui-settings": "Išplėstiniai UI nustatymai", + "disable-on-property": "Išjungti pagal savybę", + "disable-on-property-none": "Nėra (laukas visada įjungtas)", + "display-condition-function": "Rodymo sąlygos funkcija", + "sub-label": "Papildoma etiketė", + "vertical-divider-after": "Vertikalus skirtukas po elemento", + "input-field-suffix": "Įvesties lauko sufiksas", + "property-row-classes": "Savybės eilutės CSS klasės", + "property-field-classes": "Savybės lauko CSS klasės", + "not-unique-property-ids-error": "Savybių ID turi būti unikalūs!", + "enable-multiple-select": "Įjungti daugybinį pasirinkimą", + "allow-empty-select-option": "Leisti tuščią pasirinkimo reikšmę", + "select-options": "Pasirinkimo parinktys", + "not-unique-select-option-value-error": "Pasirinkimo reikšmės turi būti unikalios!", + "value": "Reikšmė", + "label": "Etiketė", + "add-option": "Pridėti parinktį", + "no-options": "Parinkčių nėra", + "remove-option": "Pašalinti parinktį", + "textarea-rows": "Teksto srities eilučių skaičius", + "help-id": "Pagalbos ID", + "buttons-direction": "Mygtukų išdėstymo kryptis", + "direction-row": "Eilutėje", + "direction-column": "Stulpelyje", + "radio-button-options": "Žymimųjų mygtukų parinktys", + "datetime-type": "Datos/laiko lauko tipas", + "datetime-type-date": "Data", + "datetime-type-time": "Laikas", + "datetime-type-datetime": "Data ir laikas", + "enable-clear-button": "Įjungti išvalymo mygtuką", + "html-section-settings": "HTML sekcijos nustatymai", + "html-section-classes": "HTML sekcijos CSS klasės", + "html-section-content": "HTML sekcijos turinys", + "array-item": "Masyvo elementas", + "item-type": "Elemento tipas", + "item-name": "Elemento pavadinimas", + "no-items": "Elementų nėra", + "support-unit-conversion": "Palaikyti vienetų konvertavimą" + }, + "clear-form": "Išvalyti formą", + "clear-form-prompt": "Ar tikrai norite pašalinti visas formos savybes?", + "import-form": "Importuoti formą iš JSON", + "export-form": "Eksportuoti formą į JSON", + "json-file": "JSON failas", + "json-content": "JSON turinys", + "invalid-form-json-file-error": "Nepavyko importuoti formos iš JSON: neteisinga formos JSON duomenų struktūra." + }, + "asset-profile": { + "asset-profile": "Turto profilis", + "asset-profiles": "Turto profiliai", + "all-asset-profiles": "Visi turto profiliai", + "add": "Pridėti turto profilį", + "edit": "Redaguoti turto profilį", + "asset-profile-details": "Turto profilio informacija", + "no-asset-profiles-text": "Turto profilių nerasta", + "search": "Ieškoti turto profilių", + "selected-asset-profiles": "{ count, plural, =1 {1 turto profilis} other {# turto profiliai} } pasirinkta", + "no-asset-profiles-matching": "Turto profilio, atitinkančio '{{entity}}', nerasta.", + "asset-profile-required": "Turto profilis būtinas.", + "idCopiedMessage": "Turto profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip numatytąjį turto profilį", + "delete": "Pašalinti turto profilį", + "copyId": "Kopijuoti turto profilio ID", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "new-device-profile-name": "Turto profilio pavadinimas", + "new-device-profile-name-required": "Turto profilio pavadinimas būtinas.", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "image": "Turto profilio paveikslėlis", + "description": "Aprašymas", + "default": "Numatytasis", + "default-rule-chain": "Numatytoji taisyklių grandinė", + "default-edge-rule-chain": "Numatytoji Edge taisyklių grandinė", + "default-edge-rule-chain-hint": "Naudojama Edge aplinkoje apdoroti gaunamus duomenis turtui, kuris priklauso šiam profiliui.", + "mobile-dashboard": "Mobilusis skydelis", + "mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip turto informacijos skydelis.", + "select-queue-hint": "Pasirinkite iš išskleidžiamojo sąrašo.", + "delete-asset-profile-title": "Ar tikrai norite pašalinti turto profilį '{{assetProfileName}}'?", + "delete-asset-profile-text": "Būkite atsargūs, po patvirtinimo turto profilis ir visi su juo susiję duomenys bus negrįžtamai pašalinti.", + "delete-asset-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 turto profilį} other {# turto profilius} }?", + "delete-asset-profiles-text": "Būkite atsargūs, po patvirtinimo visi pasirinkti turto profiliai ir susiję duomenys bus negrįžtamai pašalinti.", + "set-default-asset-profile-title": "Ar tikrai norite nustatyti turto profilį '{{assetProfileName}}' kaip numatytąjį?", + "set-default-asset-profile-text": "Po patvirtinimo šis turto profilis bus pažymėtas kaip numatytasis ir bus naudojamas naujiems turtams, kuriems nenurodytas profilis.", + "no-asset-profiles-found": "Turto profilių nerasta.", + "create-new-asset-profile": "Sukurti naują!", + "create-asset-profile": "Sukurti naują turto profilį", + "import": "Importuoti turto profilį", + "export": "Eksportuoti turto profilį", + "export-failed-error": "Nepavyko eksportuoti turto profilio: {{error}}", + "asset-profile-file": "Turto profilio failas", + "invalid-asset-profile-file-error": "Nepavyko importuoti turto profilio: neteisinga duomenų struktūra." + }, + "device-profile": { + "device-profile": "Įrenginio profilis", + "device-profiles": "Įrenginių profiliai", + "all-device-profiles": "Visi įrenginių profiliai", + "add": "Pridėti įrenginio profilį", + "edit": "Redaguoti įrenginio profilį", + "device-profile-details": "Įrenginio profilio informacija", + "no-device-profiles-text": "Įrenginių profilių nerasta", + "search": "Ieškoti įrenginių profilių", + "selected-device-profiles": "{ count, plural, =1 {1 įrenginio profilis} other {# įrenginių profiliai} } pasirinkta", + "no-device-profiles-matching": "Įrenginio profilio, atitinkančio '{{entity}}', nerasta.", + "device-profile-required": "Įrenginio profilis būtinas.", + "idCopiedMessage": "Įrenginio profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip numatytąjį įrenginio profilį", + "delete": "Pašalinti įrenginio profilį", + "copyId": "Kopijuoti įrenginio profilio ID", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "type": "Profilio tipas", + "type-required": "Profilio tipas būtinas.", + "type-default": "Numatytasis", + "image": "Įrenginio profilio paveikslėlis", + "transport-type": "Perdavimo tipas", + "transport-type-required": "Perdavimo tipas būtinas.", + "transport-type-default": "Numatytasis", + "transport-type-default-hint": "Palaiko pagrindinius MQTT, HTTP ir CoAP perdavimo tipus.", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Įjungia išplėstinius MQTT perdavimo nustatymus.", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "Įjungia išplėstinius CoAP perdavimo nustatymus.", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M perdavimo tipas.", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "Nustatykite SNMP perdavimo konfigūraciją.", + "transport-type-http": "HTTP", + "description": "Aprašymas", + "default": "Numatytasis", + "profile-configuration": "Profilio konfigūracija", + "transport-configuration": "Perdavimo konfigūracija", + "default-rule-chain": "Numatytoji taisyklių grandinė", + "default-edge-rule-chain": "Numatytoji Edge taisyklių grandinė", + "default-edge-rule-chain-hint": "Naudojama Edge aplinkoje apdoroti duomenis, gaunamus iš įrenginių, naudojančių šį profilį.", + "mobile-dashboard": "Mobilusis skydelis", + "mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip įrenginio informacijos skydelis.", + "select-queue-hint": "Pasirinkite iš išskleidžiamojo sąrašo.", + "delete-device-profile-title": "Ar tikrai norite pašalinti įrenginio profilį '{{deviceProfileName}}'?", + "delete-device-profile-text": "Būkite atsargūs, po patvirtinimo įrenginio profilis ir visi susiję duomenys, įskaitant OTA atnaujinimus, bus negrįžtamai pašalinti.", + "delete-device-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 įrenginio profilį} other {# įrenginių profilius} }?", + "delete-device-profiles-text": "Būkite atsargūs, po patvirtinimo visi pasirinkti įrenginių profiliai ir susiję duomenys, įskaitant OTA atnaujinimus, bus negrįžtamai pašalinti.", + "set-default-device-profile-title": "Ar tikrai norite nustatyti įrenginio profilį '{{deviceProfileName}}' kaip numatytąjį?", + "set-default-device-profile-text": "Po patvirtinimo šis įrenginio profilis bus pažymėtas kaip numatytasis ir bus naudojamas naujiems įrenginiams, kuriems profilis nenurodytas.", + "no-device-profiles-found": "Įrenginių profilių nerasta.", + "create-new-device-profile": "Sukurti naują!", + "mqtt-device-topic-filters": "MQTT įrenginių temų filtrai", + "mqtt-device-topic-filters-unique": "MQTT įrenginių temų filtrai turi būti unikalūs.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) mazgas.", + "mqtt-device-topic-filters-spark-plug-hint": "Leidžia prisijungimus iš EoN mazgų, naudojančių Sparkplug B formatą ir temų struktūrą.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metrikos, saugomos kaip atributai.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "SparkPlug metrikų pavadinimai, kurie bus saugomi kaip įrenginio atributai. Visos kitos metrikos bus saugomos kaip telemetrijos duomenys.", + "mqtt-device-payload-type": "MQTT įrenginio duomenų formatas", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Įjungti suderinamumą su kitais duomenų formatais.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Įjungus šią parinktį, platforma pagal nutylėjimą naudos Protobuf formatą. Jei analizė nepavyks, bus naudojamas JSON. Naudinga pereinant nuo JSON prie Protobuf atnaujinant programinę įrangą. Rekomenduojama išjungti, kai visi įrenginiai bus atnaujinti.", + "mqtt-use-json-format-for-default-downlink-topics": "Naudoti JSON formatą numatytosioms atsiuntimo temoms", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Kai įjungta, platforma naudos JSON formatą siųsdama atributus ir RPC per temas v1/devices/me/.... Tai neturi įtakos naujoms (v2) temoms.", + "mqtt-send-ack-on-validation-exception": "Siųsti PUBACK, kai PUBLISH žinutės validacija nesėkminga", + "mqtt-send-ack-on-validation-exception-hint": "Pagal numatymą platforma nutraukia MQTT sesiją, kai žinutės validacija nesėkminga. Įjungus šią parinktį, vietoj to bus siunčiamas patvirtinimas.", + "mqtt-protocol-version": "Protokolo versija", + "snmp-add-mapping": "Pridėti SNMP atvaizdavimą", + "snmp-mapping-not-configured": "OID atvaizdavimas į telemetriją/atributus nesukonfigūruotas", + "snmp-timseries-or-attribute-name": "Telemetrijos/atributo pavadinimas atvaizdavimui", + "snmp-timseries-or-attribute-type": "Telemetrijos/atributo tipas atvaizdavimui", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Duomenų formatas būtinas.", + "coap-device-type": "CoAP įrenginio tipas", + "coap-device-payload-type": "CoAP įrenginio duomenų formatas", + "coap-device-type-required": "CoAP įrenginio tipas būtinas.", + "coap-device-type-default": "Numatytasis", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "Palaikomi vieno lygmens [+] ir kelių lygių [#] pakaitos simboliai.", + "telemetry-topic-filter": "Telemetrijos temų filtras", + "telemetry-topic-filter-required": "Telemetrijos temų filtras būtinas.", + "attributes-topic-filter": "Atributų publikavimo temų filtras", + "attributes-subscribe-topic-filter": "Atributų prenumeratos temų filtras", + "attributes-topic-filter-required": "Atributų publikavimo temų filtras būtinas.", + "attributes-subscribe-topic-filter-required": "Atributų prenumeratos tema būtina.", + "telemetry-proto-schema": "Telemetrijos Protobuf schema", + "telemetry-proto-schema-required": "Telemetrijos Protobuf schema būtina.", + "attributes-proto-schema": "Atributų Protobuf schema", + "attributes-proto-schema-required": "Atributų Protobuf schema būtina.", + "rpc-response-proto-schema": "RPC atsakymo Protobuf schema", + "rpc-response-proto-schema-required": "RPC atsakymo Protobuf schema būtina.", + "rpc-response-topic-filter": "RPC atsakymo temų filtras", + "rpc-response-topic-filter-required": "RPC atsakymo temų filtras būtinas.", + "rpc-request-proto-schema": "RPC užklausos Protobuf schema", + "rpc-request-proto-schema-required": "RPC užklausos Protobuf schema būtina.", + "rpc-request-proto-schema-hint": "RPC užklausos žinutėje visada turi būti laukeliai: string method = 1; int32 requestId = 2; ir params = 3 (bet kokio tipo).", + "not-valid-pattern-topic-filter": "Netinkamas temų filtro šablonas.", + "not-valid-single-character": "Netinkamas vieno lygmens pakaitos simbolio naudojimas.", + "not-valid-multi-character": "Netinkamas kelių lygių pakaitos simbolio naudojimas.", + "single-level-wildcards-hint": "[+] galima naudoti bet kuriame temų filtro lygyje, pvz. v1/devices/+/telemetry.", + "multi-level-wildcards-hint": "[#] turi būti paskutinis simbolis, pvz. # arba v1/devices/me/#.", + "alarm-rules": "Įspėjimų taisyklės", + "alarm-rules-with-count": "Įspėjimų taisyklės ({{count}})", + "no-alarm-rules": "Įspėjimų taisyklių nesukonfigūruota.", + "add-alarm-rule": "Pridėti įspėjimo taisyklę", + "edit-alarm-rule": "Redaguoti įspėjimo taisyklę", + "alarm-type": "Įspėjimo tipas", + "alarm-type-required": "Įspėjimo tipas būtinas.", + "alarm-type-unique": "Įspėjimo tipas turi būti unikalus šiame profilyje.", + "alarm-type-max-length": "Įspėjimo tipo pavadinimas turi būti trumpesnis nei 256 simboliai.", + "create-alarm-pattern": "Sukurti {{alarmType}} įspėjimą", + "create-alarm-rules": "Sukurti įspėjimo taisykles", + "no-create-alarm-rules": "Sukūrimo sąlygų nėra.", + "add-create-alarm-rule-prompt": "Pridėkite įspėjimo sukūrimo sąlygą", + "clear-alarm-rule": "Išvalyti įspėjimo taisyklę", + "no-clear-alarm-rule": "Nėra išvalymo sąlygų", + "add-create-alarm-rule": "Pridėti sukūrimo sąlygą", + "add-clear-alarm-rule": "Pridėti išvalymo sąlygą", + "select-alarm-severity": "Pasirinkite įspėjimo rimtumą", + "alarm-severity-required": "Įspėjimo rimtumas būtinas.", + "condition-duration": "Sąlygos trukmė", + "condition-duration-value": "Trukmės reikšmė", + "condition-duration-time-unit": "Laiko vienetas", + "condition-duration-value-range": "Trukmės reikšmė turi būti nuo 1 iki 2147483647.", + "condition-duration-value-pattern": "Trukmės reikšmė turi būti sveikasis skaičius.", + "condition-duration-value-required": "Trukmės reikšmė būtina.", + "condition-duration-time-unit-required": "Laiko vienetas būtinas.", + "advanced-settings": "Išplėstiniai nustatymai", + "alarm-rule-additional-info": "Papildoma informacija", + "edit-alarm-rule-additional-info": "Redaguoti papildomą informaciją", + "alarm-rule-additional-info-placeholder": "Pateikite komentarus ar pastabas, kurios bus rodomos įspėjimo informacijos skiltyje „Papildoma informacija“.", + "alarm-rule-additional-info-hint": "Patarimas: naudokite ${keyName} norėdami įtraukti atributų ar telemetrijos reikšmes.", + "alarm-rule-mobile-dashboard": "Mobilusis įspėjimų skydelis", + "alarm-rule-mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip įspėjimo detalių skydelis.", + "alarm-rule-no-mobile-dashboard": "Skydelis nepasirinktas.", + "propagate-alarm": "Platinti įspėjimą susijusiems subjektams", + "alarm-rule-relation-types-list": "Ryšių tipai, kuriems platinti įspėjimą", + "alarm-rule-relation-types-list-hint": "Jei nenurodyta, įspėjimai bus platinami be filtravimo pagal ryšių tipą.", + "propagate-alarm-to-owner": "Platinti įspėjimą subjekto savininkui (klientui ar nuomininkui)", + "propagate-alarm-to-tenant": "Platinti įspėjimą nuomininkui", + "alarm-rule-condition": "Įspėjimo sąlyga", + "enter-alarm-rule-condition-prompt": "Pridėkite įspėjimo sąlygą", + "edit-alarm-rule-condition": "Redaguoti įspėjimo sąlygą", + "device-provisioning": "Įrenginių paruošimas (provisioning)", + "provision-strategy": "Paruošimo strategija", + "provision-strategy-required": "Paruošimo strategija būtina.", + "provision-strategy-disabled": "Išjungta", + "provision-strategy-created-new": "Leisti kurti naujus įrenginius", + "provision-strategy-check-pre-provisioned": "Tikrinti iš anksto paruoštus įrenginius", + "provision-device-key": "Įrenginio paruošimo raktas", + "provision-device-key-required": "Įrenginio paruošimo raktas būtinas.", + "copy-provision-key": "Kopijuoti paruošimo raktą", + "provision-key-copied-message": "Paruošimo raktas nukopijuotas į iškarpinę.", + "provision-device-secret": "Įrenginio paruošimo slaptas kodas", + "provision-device-secret-required": "Įrenginio paruošimo slaptas kodas būtinas.", + "copy-provision-secret": "Kopijuoti slaptą kodą", + "provision-secret-copied-message": "Slaptas kodas nukopijuotas į iškarpinę.", + "provision-strategy-x509": { + "certificate-chain": "X509 sertifikatų grandinė", + "certificate-chain-hint": "X.509 strategija naudojama įrenginiams paruošti naudojant dvipusį TLS ryšį ir kliento sertifikatus.", + "allow-create-new-devices": "Leisti kurti naujus įrenginius", + "allow-create-new-devices-hint": "Jei pažymėta, nauji įrenginiai bus kuriami, o kliento sertifikatas naudojamas kaip įrenginio įgaliojimai.", + "certificate-value": "Sertifikatas PEM formatu", + "certificate-value-required": "Sertifikatas PEM formatu būtinas.", + "cn-regex-variable": "CN reguliariosios išraiškos kintamasis", + "cn-regex-variable-required": "CN reguliariosios išraiškos kintamasis būtinas.", + "cn-regex-variable-hint": "Naudojamas įrenginio pavadinimui gauti iš X509 sertifikato 'Common Name' lauko." + }, + "condition": "Sąlyga", + "condition-type": "Sąlygos tipas", + "condition-type-simple": "Paprasta", + "condition-type-duration": "Trukmės", + "condition-during": "Per {{during}}", + "condition-during-dynamic": "Per „{{attribute}}“ ({{during}})", + "condition-type-repeating": "Pasikartojanti", + "condition-type-required": "Sąlygos tipas būtinas.", + "condition-repeating-value": "Įvykių skaičius", + "condition-repeating-value-range": "Įvykių skaičius turi būti nuo 1 iki 2147483647.", + "condition-repeating-value-pattern": "Įvykių skaičius turi būti sveikasis skaičius.", + "condition-repeating-value-required": "Įvykių skaičius būtinas.", + "condition-repeat-times": "Kartojasi { count, plural, =1 {1 kartą} other {# kartus} }", + "condition-repeat-times-dynamic": "Kartojasi „{attribute}“ ({ count, plural, =1 {1 kartą} other {# kartus} })", + "schedule-type": "Tvarkaraščio tipas", + "schedule-type-required": "Tvarkaraščio tipas būtinas.", + "schedule": "Tvarkaraštis", + "edit-schedule": "Redaguoti įspėjimo tvarkaraštį", + "schedule-any-time": "Aktyvus visą laiką", + "schedule-specific-time": "Aktyvus tam tikru laiku", + "schedule-custom": "Pasirinktinis", + "schedule-day": { + "monday": "Pirmadienis", + "tuesday": "Antradienis", + "wednesday": "Trečiadienis", + "thursday": "Ketvirtadienis", + "friday": "Penktadienis", + "saturday": "Šeštadienis", + "sunday": "Sekmadienis" + }, + "schedule-days": "Dienos", + "schedule-time": "Laikas", + "schedule-time-from": "Nuo", + "schedule-time-to": "Iki", + "schedule-days-of-week-required": "Reikia pasirinkti bent vieną savaitės dieną.", + "create-device-profile": "Sukurti naują įrenginio profilį", + "import": "Importuoti įrenginio profilį", + "export": "Eksportuoti įrenginio profilį", + "export-failed-error": "Nepavyko eksportuoti įrenginio profilio: {{error}}", + "device-profile-file": "Įrenginio profilio failas", + "invalid-device-profile-file-error": "Nepavyko importuoti įrenginio profilio: neteisinga profilio duomenų struktūra.", + "power-saving-mode": "Energijos taupymo režimas", + "power-saving-mode-type": { + "default": "Naudoti įrenginio profilio energijos taupymo režimą", + "psm": "Power Saving Mode (PSM)", + "drx": "Discontinuous Reception (DRX)", + "edrx": "Extended Discontinuous Reception (eDRX)" + }, + "edrx-cycle": "eDRX ciklas", + "edrx-cycle-required": "eDRX ciklas būtinas.", + "edrx-cycle-pattern": "eDRX ciklas turi būti teigiamas sveikasis skaičius.", + "edrx-cycle-min": "Minimalus eDRX ciklo skaičius – {{ min }} sekundžių.", + "paging-transmission-window": "Puslapiavimo perdavimo langas", + "paging-transmission-window-required": "Puslapiavimo perdavimo langas būtinas.", + "paging-transmission-window-pattern": "Puslapiavimo perdavimo langas turi būti teigiamas sveikasis skaičius.", + "paging-transmission-window-min": "Minimalus puslapiavimo perdavimo langas – {{ min }} sekundžių.", + "psm-activity-timer": "PSM aktyvumo laikmatis", + "psm-activity-timer-required": "PSM aktyvumo laikmatis būtinas.", + "psm-activity-timer-pattern": "PSM aktyvumo laikmatis turi būti teigiamas sveikasis skaičius.", + "psm-activity-timer-min": "Minimalus PSM aktyvumo laikmatis – {{ min }} sekundžių.", + "lwm2m": { + "object-list": "Objektų sąrašas", + "object-list-empty": "Objektai nepasirinkti.", + "no-objects-found": "Objektų nerasta.", + "no-objects-matching": "Objektų, atitinkančių '{{object}}', nerasta.", + "model-tab": "LWM2M modelis", + "add-new-instances": "Pridėti naujus egzempliorius", + "instances-list": "Egzempliorių sąrašas", + "instances-list-required": "Egzempliorių sąrašas būtinas.", + "instance-id-pattern": "Egzemplioriaus ID turi būti teigiamas sveikasis skaičius.", + "instance-id-max": "Didžiausia egzemplioriaus ID reikšmė – {{max}}.", + "instance": "Egzempliorius", + "resource-label": "#ID Išteklius (Resource name)", + "observe-label": "Stebėjimas", + "attribute-label": "Atributas", + "telemetry-label": "Telemetrija", + "edit-observe-select": "Norėdami redaguoti stebėjimą, pasirinkite telemetriją arba atributą", + "edit-attributes-select": "Norėdami redaguoti atributus, pasirinkite telemetriją arba atributą", + "no-attributes-set": "Atributai nenustatyti", + "key-name": "Rakto pavadinimas", + "key-name-required": "Rakto pavadinimas būtinas.", + "attribute-name": "Atributo pavadinimas", + "attribute-name-required": "Atributo pavadinimas būtinas.", + "attribute-value": "Atributo reikšmė", + "attribute-value-required": "Atributo reikšmė būtina.", + "attribute-value-pattern": "Atributo reikšmė turi būti teigiamas sveikasis skaičius.", + "edit-attributes": "Redaguoti atributus: {{ name }}", + "view-attributes": "Peržiūrėti atributus: {{ name }}", + "add-attribute": "Pridėti atributą", + "edit-attribute": "Redaguoti atributą", + "view-attribute": "Peržiūrėti atributą", + "remove-attribute": "Pašalinti atributą", + "delete-server-text": "Būkite atsargūs – po patvirtinimo serverio konfigūracija bus negrįžtamai prarasta.", + "delete-server-title": "Ar tikrai norite pašalinti serverį?", + "mode": "Saugumo konfigūracijos režimas", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Bootstrap Server (Trumpasis ID...)", + "lwm2m-server-legend": "LwM2M Server (Trumpasis ID...)", + "server": "Serveris", + "short-id": "Trumpasis serverio ID", + "short-id-tooltip": "Trumpasis serverio ID naudojamas LwM2M serverio egzemplioriaus susiejimui.\nŠis identifikatorius unikalus kiekvienam LwM2M klientui.\nReikšmė privaloma, kai Bootstrap-Server parametras yra 'false'.\nVertės ID:0 ir ID:65535 negali būti naudojamos LwM2M serverio identifikavimui.", + "short-id-tooltip-bootstrap": "Trumpasis serverio ID naudojamas LwM2M serverio egzemplioriaus susiejimui.\nPrivaloma reikšmė, kai Bootstrap-Server parametras yra 'false'.", + "short-id-required": "Trumpasis serverio ID būtinas.", + "short-id-range": "Trumpasis serverio ID turi būti tarp {{ min }} ir {{ max }}.", + "short-id-pattern": "Trumpasis serverio ID turi būti teigiamas sveikasis skaičius.", + "lifetime": "Kliento registracijos trukmė", + "lifetime-required": "Kliento registracijos trukmė būtina.", + "lifetime-pattern": "Kliento registracijos trukmė turi būti teigiamas sveikasis skaičius.", + "default-min-period": "Minimalus laikotarpis tarp dviejų pranešimų (s)", + "default-min-period-tooltip": "Numatytoji vertė, kurią LwM2M klientas turėtų naudoti minimaliam stebėjimo laikotarpiui, jei parametras nenurodytas.", + "default-min-period-required": "Minimalus laikotarpis būtinas.", + "default-min-period-pattern": "Minimalus laikotarpis turi būti teigiamas sveikasis skaičius.", + "notification-storing": "Pranešimų saugojimas, kai įrenginys išjungtas arba neprisijungęs", + "binding": "Susiejimas (Binding)", + "binding-type": { + "u": "U: Klientas pasiekiamas per UDP ryšį bet kuriuo metu.", + "m": "M: Klientas pasiekiamas per MQTT ryšį bet kuriuo metu.", + "h": "H: Klientas pasiekiamas per HTTP ryšį bet kuriuo metu.", + "t": "T: Klientas pasiekiamas per TCP ryšį bet kuriuo metu.", + "s": "S: Klientas pasiekiamas per SMS ryšį bet kuriuo metu.", + "n": "N: Klientas turi siųsti atsakymą naudodamas Non-IP ryšį (palaikoma nuo LWM2M 1.1).", + "uq": "UQ: UDP ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1).", + "uqs": "UQS: aktyvus tiek UDP, tiek SMS ryšys; UDP eilės režimu, SMS standartiniu režimu (nepalaikoma nuo LWM2M 1.1).", + "tq": "TQ: TCP ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1).", + "tqs": "TQS: aktyvus tiek TCP, tiek SMS ryšys; TCP eilės režimu, SMS standartiniu režimu (nepalaikoma nuo LWM2M 1.1).", + "sq": "SQ: SMS ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1)." + }, + "binding-tooltip": "Tai yra sąrašas iš LwM2M serverio objekto „binding“ ištekliaus - /1/x/7.\nNurodo palaikomus ryšio (binding) režimus LwM2M kliente.\nŠi reikšmė TURĖTŲ sutapti su „Supported Binding and Modes“ ištekliaus reikšme įrenginio objekte (/3/0/16).\nNors palaikomi keli transportavimo protokolai, visos sesijos metu galima naudoti tik vieną ryšio būdą.\nPavyzdžiui, kai palaikomi UDP ir SMS, LwM2M klientas ir serveris gali naudoti tik vieną — arba UDP, arba SMS — visos sesijos metu.", + "bootstrap-server": "Bootstrap serveris", + "lwm2m-server": "LwM2M serveris", + "include-bootstrap-server": "Įtraukti Bootstrap serverio atnaujinimus", + "bootstrap-update-title": "Bootstrap serveris jau sukonfigūruotas. Ar tikrai norite pašalinti atnaujinimus?", + "bootstrap-update-text": "Būkite atsargūs — po patvirtinimo Bootstrap serverio konfigūracija bus negrįžtamai prarasta.", + "server-host": "Serverio adresas (Host)", + "server-host-required": "Serverio adresas būtinas.", + "server-port": "Prievadas (Port)", + "server-port-required": "Prievadas būtinas.", + "server-port-pattern": "Prievadas turi būti teigiamas sveikasis skaičius.", + "server-port-range": "Prievadas turi būti tarp 1 ir 65535.", + "server-public-key": "Serverio viešasis raktas", + "server-public-key-required": "Serverio viešasis raktas būtinas.", + "client-hold-off-time": "Kliento atidėjimo laikas (Hold Off Time)", + "client-hold-off-time-required": "Kliento atidėjimo laikas būtinas.", + "client-hold-off-time-pattern": "Kliento atidėjimo laikas turi būti teigiamas sveikasis skaičius.", + "client-hold-off-time-tooltip": "Naudojamas tik su Bootstrap serveriu – nurodo laukimo laiką prieš registraciją.", + "account-after-timeout": "Sąskaita po pasibaigusio laiko (Account after timeout)", + "account-after-timeout-required": "„Account after timeout“ reikšmė būtina.", + "account-after-timeout-pattern": "„Account after timeout“ turi būti teigiamas sveikasis skaičius.", + "account-after-timeout-tooltip": "Bootstrap serverio „Account after timeout“ reikšmė, apibrėžianti veiksmą po nurodyto laiko.", + "server-type": "Serverio tipas", + "add-new-server-title": "Pridėti naują serverio konfigūraciją", + "add-server-config": "Pridėti serverio konfigūraciją", + "add-lwm2m-server-config": "Pridėti LwM2M serverį", + "no-config-servers": "Serverių konfigūracijų nėra", + "others-tab": "Kiti nustatymai", + "ota-update": "OTA atnaujinimas", + "use-object-19-for-ota-update": "Naudoti objektą 19 OTA failo metaduomenims (kontrolinė suma, dydis, versija, pavadinimas)", + "use-object-19-for-ota-update-hint": "Naudokite išteklių ObjectId = 19 OTA atnaujinimams: Firmware → InstanceId = 65534, Software → InstanceId = 65535. Duomenų formatas – JSON, užkoduotas Base64 formatu. JSON faile pateikiama OTA metainformacija: „Checksum“ (SHA256). Papildomi laukai: „Title“ (pavadinimas), „Version“ (versija), „File Name“ (failo pavadinimas), „File Size“ (dydis baitais).", + "client-strategy": "Kliento strategija jungiantis", + "client-strategy-label": "Strategija", + "client-strategy-only-observe": "Tik „Observe“ užklausos klientui po pradinio prisijungimo", + "client-strategy-read-all": "Perskaityti visus išteklius ir siųsti „Observe“ užklausas po registracijos", + "fw-update": "Programinės aparatinės įrangos (Firmware) atnaujinimas", + "fw-update-strategy": "Firmware atnaujinimo strategija", + "fw-update-strategy-data": "Įkelti firmware kaip dvejetainį failą naudojant objektą 19 ir išteklių 0 (Data)", + "fw-update-strategy-package": "Įkelti firmware kaip dvejetainį failą naudojant objektą 5 ir išteklių 0 (Package)", + "fw-update-strategy-package-uri": "Automatiškai sugeneruoti unikalų CoAP URL firmware parsisiuntimui ir naudoti objektą 5 bei išteklių 1 (Package URI)", + "sw-update": "Programinės įrangos (Software) atnaujinimas", + "sw-update-strategy": "Software atnaujinimo strategija", + "sw-update-strategy-package": "Įkelti dvejetainį failą naudojant objektą 9 ir išteklių 2 (Package)", + "sw-update-strategy-package-uri": "Automatiškai sugeneruoti unikalų CoAP URL programos parsisiuntimui ir naudoti objektą 9 bei išteklių 3 (Package URI)", + "fw-update-resource": "Firmware atnaujinimo CoAP išteklius", + "fw-update-resource-required": "Firmware atnaujinimo CoAP išteklius būtinas.", + "sw-update-resource": "Software atnaujinimo CoAP išteklius", + "sw-update-resource-required": "Software atnaujinimo CoAP išteklius būtinas.", + "config-json-tab": "JSON konfigūracijos profilis", + "attributes-name": { + "min-period": "Minimalus periodas", + "max-period": "Maksimalus periodas", + "greater-than": "Didesnis nei", + "less-than": "Mažesnis nei", + "step": "Žingsnis", + "min-evaluation-period": "Minimalus vertinimo periodas", + "max-evaluation-period": "Maksimalus vertinimo periodas" + }, + "default-object-id": "Numatytoji Objekto Versija (Atributas)", + "default-object-id-ver": { + "v1-0": "1.0", + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Stebėjimo strategija", + "single": "Atskiras", + "single-description": "Vienas stebėjimo užklausimas vienam ištekliui (didesnis tikslumas, daugiau tinklo srauto)", + "composite-all": "Bendras (visi ištekliai)", + "composite-all-description": "Visi ištekliai stebimi naudojant vieną bendrą stebėjimo užklausą (efektyviau, bet mažiau lankstu)", + "composite-by-object": "Pagal objektus", + "composite-by-object-description": "Ištekliai grupuojami pagal objektų tipus ir stebimi naudojant atskiras bendras stebėjimo užklausas (subalansuotas metodas)" + } + }, + "snmp": { + "add-communication-config": "Pridėti ryšio konfigūraciją", + "add-mapping": "Pridėti susiejimą", + "authentication-passphrase": "Autentifikacijos slaptažodis", + "authentication-passphrase-required": "Autentifikacijos slaptažodis būtinas.", + "authentication-protocol": "Autentifikacijos protokolas", + "authentication-protocol-required": "Autentifikacijos protokolas būtinas.", + "communication-configs": "Ryšio konfigūracijos", + "community": "Bendruomenės eilutė (Community string)", + "community-required": "Bendruomenės eilutė būtina.", + "context-name": "Konteksto pavadinimas", + "data-key": "Duomenų raktas", + "data-key-required": "Duomenų raktas būtinas.", + "data-type": "Duomenų tipas", + "data-type-required": "Duomenų tipas būtinas.", + "engine-id": "Variklio ID (Engine ID)", + "host": "Serveris / įrenginys (Host)", + "host-required": "Serverio / įrenginio pavadinimas būtinas.", + "oid": "OID (Objekto identifikatorius)", + "oid-pattern": "Neteisingas OID formatas.", + "oid-required": "OID būtinas.", + "please-add-communication-config": "Pridėkite ryšio konfigūraciją.", + "please-add-mapping-config": "Pridėkite susiejimo konfigūraciją.", + "port": "Prievadas (Port)", + "port-format": "Neteisingas prievado formatas.", + "port-required": "Prievadas būtinas.", + "privacy-passphrase": "Privatumo slaptažodis", + "privacy-passphrase-required": "Privatumo slaptažodis būtinas.", + "privacy-protocol": "Privatumo protokolas", + "privacy-protocol-required": "Privatumo protokolas būtinas.", + "protocol-version": "Protokolo versija", + "protocol-version-required": "Protokolo versija būtina.", + "querying-frequency": "Užklausų dažnis, ms", + "querying-frequency-invalid-format": "Užklausų dažnis turi būti teigiamas sveikasis skaičius.", + "querying-frequency-required": "Užklausų dažnis būtinas.", + "retries": "Bandymų skaičius (Retries)", + "retries-invalid-format": "Bandymų skaičius turi būti teigiamas sveikasis skaičius.", + "retries-required": "Bandymų skaičius būtinas.", + "scope": "Apimtis (Scope)", + "scope-required": "Apimtis būtina.", + "security-name": "Saugos pavadinimas", + "security-name-required": "Saugos pavadinimas būtinas.", + "timeout-ms": "Laiko limitas, ms", + "timeout-ms-invalid-format": "Laiko limitas turi būti teigiamas sveikasis skaičius.", + "timeout-ms-required": "Laiko limitas būtinas.", + "user-name": "Vartotojo vardas", + "user-name-required": "Vartotojo vardas būtinas." + } + }, + "dialog": { + "close": "Uždaryti dialogo langą", + "error-message-title": "Klaidos pranešimas:", + "error-details-title": "Informacija apie klaidą" + }, + "direction": { + "column": "Stulpelis", + "row": "Eilutė" + }, + "edge": { + "edge": "Edge įrenginys", + "edge-instances": "Edge instancijos", + "instances": "Instancijos", + "edge-file": "Edge failas", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "label-max-length": "Etiketė negali viršyti 256 simbolių.", + "type-max-length": "Tipas negali viršyti 256 simbolių.", + "management": "Edge valdymas", + "no-edges-matching": "Edge įrenginių, atitinkančių '{{entity}}', nerasta.", + "add": "Pridėti Edge įrenginį", + "no-edges-text": "Edge įrenginių nerasta.", + "edge-details": "Edge įrenginio informacija", + "add-edge-text": "Pridėti naują Edge įrenginį", + "delete": "Panaikinti Edge įrenginį", + "delete-edge-title": "Ar tikrai norite panaikinti Edge įrenginį '{{edgeName}}'?", + "delete-edge-text": "Po patvirtinimo Edge įrenginys ir visa su juo susijusi informacija bus negrįžtamai panaikinta.", + "delete-edges-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 Edge įrenginį} other {# Edge įrenginius} }?", + "delete-edges-text": "Po patvirtinimo visi pasirinkti Edge įrenginiai ir jų duomenys bus negrįžtamai panaikinti.", + "name": "Pavadinimas", + "name-starts-with": "Edge įrenginio pavadinimas prasideda", + "name-required": "Pavadinimas būtinas.", + "description": "Aprašymas", + "details": "Išsami informacija", + "events": "Įvykiai", + "copy-id": "Kopijuoti Edge ID", + "id-copied-message": "Edge ID nukopijuotas į iškarpinę", + "sync": "Sinchronizuoti Edge", + "edge-required": "Edge būtinas.", + "edge-type": "Edge tipas", + "edge-type-required": "Edge tipas būtinas.", + "event-action": "Įvykio veiksmas", + "entity-id": "Subjekto ID", + "select-edge-type": "Pasirinkite Edge tipą", + "assign-to-customer": "Priskirti klientui", + "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti Edge įrenginį (-ius)", + "assign-edge-to-customer": "Priskirti Edge įrenginį (-ius) klientui", + "assign-edge-to-customer-text": "Pasirinkite Edge įrenginius, kuriuos norite priskirti klientui", + "assignedToCustomer": "Priskirta klientui", + "edge-public": "Edge yra viešas", + "assigned-to-customer": "Priskirta: {{customerTitle}}", + "unassign-from-customer": "Atsieti nuo kliento", + "unassign-edge-title": "Ar tikrai norite atsieti Edge '{{edgeName}}'?", + "unassign-edge-text": "Po patvirtinimo Edge įrenginys bus atsietas ir nepasiekiamas klientui.", + "unassign-edges-title": "Ar tikrai norite atsieti { count, plural, =1 {1 Edge įrenginį} other {# Edge įrenginius} }?", + "unassign-edges-text": "Po patvirtinimo visi pasirinkti Edge įrenginiai bus atsieti nuo kliento.", + "make-public": "Padaryti Edge viešu", + "make-public-edge-title": "Ar tikrai norite Edge '{{edgeName}}' padaryti viešu?", + "make-public-edge-text": "Po patvirtinimo Edge ir visi jo duomenys bus prieinami kitiems vartotojams.", + "make-private": "Padaryti Edge privačiu", + "public": "Viešas", + "make-private-edge-title": "Ar tikrai norite Edge '{{edgeName}}' padaryti privačiu?", + "make-private-edge-text": "Po patvirtinimo Edge ir jo duomenys taps privatūs ir nepasiekiami kitiems vartotojams.", + "import": "Importuoti Edge", + "install-connect-instructions": "Diegimo ir prijungimo instrukcijos", + "install-connect-instructions-edge-created": "Edge sukurtas! Peržiūrėkite diegimo ir prijungimo instrukcijas.", + "loading-edge-instructions": "Įkeliamos Edge instrukcijos...", + "label": "Etiketė", + "load-entity-error": "Nepavyko įkelti duomenų. Subjektas buvo panaikintas.", + "assign-new-edge": "Priskirti naują Edge", + "unassign-from-edge": "Atsieti nuo Edge", + "edge-key": "Edge raktas", + "copy-edge-key": "Kopijuoti Edge raktą", + "edge-key-copied-message": "Edge raktas nukopijuotas į iškarpinę", + "edge-secret": "Edge slaptas raktas", + "copy-edge-secret": "Kopijuoti Edge slaptą raktą", + "edge-secret-copied-message": "Edge slaptas raktas nukopijuotas į iškarpinę", + "manage-assets": "Valdyti turtą (assets)", + "manage-devices": "Valdyti įrenginius", + "manage-entity-views": "Valdyti subjekto rodinius", + "manage-dashboards": "Valdyti skydelius", + "manage-rulechains": "Valdyti taisyklių grandines (rule chains)", + "assets": "Edge turtas", + "devices": "Edge įrenginiai", + "entity-views": "Edge subjekto rodiniai", + "dashboard": "Edge skydelis", + "dashboards": "Edge skydeliai", + "rulechain-templates": "Taisyklių grandinių šablonai", + "edge-rulechain-templates": "Edge taisyklių grandinių šablonai", + "rulechains": "Edge taisyklių grandinės", + "search": "Ieškoti Edge įrenginių", + "selected-edges": "Pasirinkta { count, plural, =1 {1 Edge įrenginys} other {# Edge įrenginiai} }", + "any-edge": "Bet kuris Edge įrenginys", + "no-edge-types-matching": "Edge tipų, atitinkančių '{{entitySubtype}}', nerasta.", + "edge-type-list-empty": "Edge tipai nepasirinkti.", + "edge-types": "Edge tipai", + "enter-edge-type": "Įveskite Edge tipą", + "deployed": "Įdiegtas", + "pending": "Laukia", + "downlinks": "Downlink ryšiai", + "no-downlinks-prompt": "Downlink ryšių nerasta.", + "sync-process-started-successfully": "Sinchronizacija sėkmingai pradėta!", + "missing-related-rule-chains-title": "Edge trūksta susijusių taisyklių grandinių", + "missing-related-rule-chains-text": "Edge priskirtos taisyklių grandinės naudoja mazgus, nukreipiančius į kitų grandinių žinutes, kurios nėra priskirtos šiam Edge.

    Trūkstamų grandinių sąrašas:
    {{missingRuleChains}}", + "widget-datasource-error": "Šis valdiklis palaiko tik EDGE subjekto duomenų šaltinius", + "upgrade-instructions": "Atnaujinimo instrukcijos.", + "connected": "Prisijungęs", + "disconnected": "Atsijungęs" + }, + "edge-event": { + "type-dashboard": "Skydelis", + "type-asset": "Turtas (Asset)", + "type-device": "Įrenginys", + "type-device-profile": "Įrenginio profilis", + "type-asset-profile": "Turto profilis", + "type-entity-view": "Subjekto rodinys", + "type-alarm": "Signalizacija (Alarm)", + "type-rule-chain": "Taisyklių grandinė (Rule Chain)", + "type-rule-chain-metadata": "Taisyklių grandinės metaduomenys", + "type-edge": "Edge įrenginys", + "type-user": "Vartotojas", + "type-tenant": "Nuomininkas (Tenant)", + "type-tenant-profile": "Nuomininko profilis", + "type-customer": "Klientas", + "type-relation": "Ryšys (Relation)", + "type-widgets-bundle": "Valdiklių paketas (Widgets Bundle)", + "type-widgets-type": "Valdiklio tipas (Widget Type)", + "type-admin-settings": "Administratoriaus nustatymai", + "type-ota-package": "OTA paketas (Over-The-Air)", + "type-queue": "Eilė (Queue)", + "action-type-added": "Pridėta", + "action-type-deleted": "Panaikinta", + "action-type-updated": "Atnaujinta", + "action-type-post-attributes": "Atributai įkelti", + "action-type-attributes-updated": "Atributai atnaujinti", + "action-type-attributes-deleted": "Atributai ištrinti", + "action-type-timeseries-updated": "Telemetrija atnaujinta", + "action-type-credentials-updated": "Įgaliojimai atnaujinti", + "action-type-assigned-to-customer": "Priskirta klientui", + "action-type-unassigned-from-customer": "Atsieta nuo kliento", + "action-type-relation-add-or-update": "Ryšys pridėtas arba atnaujintas", + "action-type-relation-deleted": "Ryšys panaikintas", + "action-type-rpc-call": "RPC iškvietimas", + "action-type-alarm-ack": "Signalizacija patvirtinta", + "action-type-alarm-clear": "Signalizacija išvalyta", + "action-type-alarm-assigned": "Signalizacija priskirta", + "action-type-alarm-unassigned": "Signalizacija atsieta", + "action-type-assigned-to-edge": "Priskirta Edge įrenginiui", + "action-type-unassigned-from-edge": "Atsieta nuo Edge įrenginio", + "action-type-credentials-request": "Įgaliojimų užklausa", + "action-type-entity-merge-request": "Subjektų sujungimo užklausa" + }, + "error": { + "unable-to-connect": "Nepavyksta prisijungti prie serverio! Patikrinkite interneto ryšį.", + "unhandled-error-code": "Neapdorotos klaidos kodas: {{errorCode}}", + "unknown-error": "Nežinoma klaida" + }, + "entity": { + "entity": "Subjektas", + "entities": "Subjektai", + "entities-count": "Subjektų kiekis", + "alarms-count": "Įspėjimų kiekis", + "aliases": "Subjektų pseudonimai", + "aliases-short": "Pseudonimai", + "entity-alias": "Subjekto pseudonimas", + "unable-delete-entity-alias-title": "Subjekto pseudonimo panaikinti nepavyko", + "unable-delete-entity-alias-text": "Subjekto pseudonimas '{{entityAlias}}' negali būti panaikintas, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "duplicate-alias-error": "Pseudonimas '{{alias}}' dubliuojasi.
    Subjektų pseudonimai skydelyje negali kartotis.", + "missing-entity-filter-error": "Trūksta pseudonimo '{{alias}}' filtro.", + "configure-alias": "Konfigūruoti pseudonimą '{{alias}}'", + "alias": "Pseudonimas", + "alias-required": "Subjekto pseudonimas būtinas.", + "remove-alias": "Pašalinti subjekto pseudonimą", + "add-alias": "Pridėti subjekto pseudonimą", + "edit-alias": "Redaguoti subjekto pseudonimą", + "entity-list": "Subjektų sąrašas", + "entity-type": "Subjekto tipas", + "entity-types": "Subjektų tipai", + "entity-type-list": "Subjektų tipų sąrašas", + "any-entity": "Bet kuris subjektas", + "add-entity-type": "Pridėti subjekto tipą", + "enter-entity-type": "Įveskite subjekto tipą", + "no-entities-matching": "Subjektų, atitinkančių '{{entity}}', nėra.", + "no-entities-text": "Subjektų nerasta", + "no-entity-types-matching": "Subjektų tipų, atitinkančių '{{entityType}}', nėra.", + "name-starts-with": "Pavadinimas prasideda", + "help-text": "Naudokite simbolį '%' pagal paieškos poreikį: '%subjekto_pavadinimo_fragmentas%', '%subjekto_pavadinimo_pabaiga', 'subjekto_pavadinimo_pradžia%'.", + "use-entity-name-filter": "Naudoti filtrą", + "entity-list-empty": "Subjektai nepasirinkti.", + "entity-type-list-required": "Bent vienas subjekto tipas turi būti pasirinktas.", + "entity-name-filter-required": "Subjekto pavadinimo filtras būtinas.", + "entity-name-filter-no-entity-matched": "Subjektų, kurių pavadinimas prasideda '{{entity}}', nėra.", + "all-subtypes": "Visi", + "select-entities": "Pasirinkite subjektus", + "no-aliases-found": "Pseudonimų nėra.", + "no-alias-matching": "Pseudonimo '{{alias}}' nėra.", + "create-new-alias": "Sukurti naują!", + "create-new": "Sukurti naują", + "key": "Raktas", + "key-name": "Rakto pavadinimas", + "no-keys-found": "Raktų nėra.", + "no-key-matching": "Rakto '{{key}}' nėra.", + "create-new-key": "Sukurti naują!", + "type": "Tipas", + "type-required": "Subjekto tipas būtinas.", + "type-device": "Įrenginys", + "type-devices": "Įrenginiai", + "list-of-devices": "{ count, plural, =1 {Vienas įrenginys} other {# įrenginių sąrašas} }", + "device-name-starts-with": "Įrenginiai, kurių pavadinimas prasideda '{{prefix}}'", + "type-device-profile": "Įrenginio profilis", + "type-device-profiles": "Įrenginių profiliai", + "clear-selected-profiles": "Išvalyti pasirinktus profilius", + "list-of-device-profiles": "{ count, plural, =1 {Vienas įrenginio profilis} other {# įrenginių profilių sąrašas} }", + "device-profile-name-starts-with": "Įrenginių profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-asset-profile": "Turto profilis", + "type-asset-profiles": "Turto profiliai", + "list-of-asset-profiles": "{ count, plural, =1 {Vienas turto profilis} other {# turto profilių sąrašas} }", + "asset-profile-name-starts-with": "Turto profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-asset": "Turtas", + "type-assets": "Turtai", + "list-of-assets": "{ count, plural, =1 {Vienas turtas} other {# turto sąrašas} }", + "asset-name-starts-with": "Turtas, kurio pavadinimas prasideda '{{prefix}}'", + "type-entity-view": "Subjekto rodinys", + "type-entity-views": "Subjektų rodiniai", + "list-of-entity-views": "{ count, plural, =1 {Vienas subjekto rodinys} other {# subjektų rodinių sąrašas} }", + "entity-view-name-starts-with": "Subjektų rodiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rule": "Taisyklė", + "type-rules": "Taisyklės", + "list-of-rules": "{ count, plural, =1 {Viena taisyklė} other {# taisyklių sąrašas} }", + "rule-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", + "type-plugin": "Papildinys", + "type-plugins": "Papildiniai", + "list-of-plugins": "{ count, plural, =1 {Vienas papildinys} other {# papildinių sąrašas} }", + "plugin-name-starts-with": "Papildiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant": "Valdytojas", + "type-tenants": "Valdytojai", + "list-of-tenants": "{ count, plural, =1 {Vienas valdytojas} other {# valdytojų sąrašas} }", + "tenant-name-starts-with": "Valdytojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant-profile": "Valdytojo profilis", + "type-tenant-profiles": "Valdytojo profiliai", + "list-of-tenant-profiles": "{ count, plural, =1 {Vienas valdytojo profilis} other {# valdytojo profilių sąrašas} }", + "tenant-profile-name-starts-with": "Valdytojo profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-customer": "Klientas", + "type-customers": "Klientai", + "list-of-customers": "{ count, plural, =1 {Vienas klientas} other {# klientų sąrašas} }", + "customer-name-starts-with": "Klientai, kurių pavadinimai prasideda '{{prefix}}'", + "type-user": "Vartotojas", + "type-users": "Vartotojai", + "list-of-users": "{ count, plural, =1 {Vienas vartotojas} other {# vartotojų sąrašas} }", + "user-name-starts-with": "Vartotojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-dashboard": "Skydelis", + "type-dashboards": "Skydeliai", + "list-of-dashboards": "{ count, plural, =1 {Vienas skydelis} other {# skydelių sąrašas} }", + "dashboard-name-starts-with": "Skydeliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-alarm": "Įspėjimas", + "type-alarms": "Įspėjimai", + "list-of-alarms": "{ count, plural, =1 {Vienas įspėjimas} other {# įspėjimų sąrašas} }", + "alarm-name-starts-with": "Įspėjimai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rulechain": "Taisyklių grandinė", + "type-rulechains": "Taisyklių grandinės", + "list-of-rulechains": "{ count, plural, =1 {Viena taisyklių grandinė} other {# taisyklių grandinių sąrašas} }", + "rulechain-name-starts-with": "Taisyklių grandinės, kurių pavadinimai prasideda '{{prefix}}'", + "type-rulenode": "Taisyklės mazgas", + "type-rulenodes": "Taisyklių mazgai", + "list-of-rulenodes": "{ count, plural, =1 {Vienas taisyklės mazgas} other {# taisyklių mazgų sąrašas} }", + "rulenode-name-starts-with": "Taisyklių mazgai, kurių pavadinimai prasideda '{{prefix}}'", + "type-current-customer": "Dabartinis klientas", + "type-current-tenant": "Dabartinis valdytojas", + "type-current-user": "Dabartinis vartotojas", + "type-current-user-owner": "Dabartinio vartotojo savininkas", + "type-calculated-field": "Apskaičiuotas laukas", + "type-calculated-fields": "Apskaičiuoti laukai", + "type-ai-model": "DI modelis", + "type-ai-models": "DI modeliai", + "type-widgets-bundle": "Valdiklių rinkinys", + "type-widgets-bundles": "Valdiklių rinkiniai", + "list-of-widgets-bundles": "{ count, plural, =1 {Vienas valdiklių rinkinys} other {# valdiklių rinkinių sąrašas} }", + "type-widget": "Valdiklis", + "type-widgets": "Valdikliai", + "list-of-widgets": "{ count, plural, =1 {Vienas valdiklis} other {# valdiklių sąrašas} }", + "search": "Subjektų paieška", + "selected-entities": "Pasirinkta { count, plural, =1 {1 subjektas} other {# subjektai} }", + "entity-name": "Subjekto pavadinimas", + "entity-label": "Subjekto etiketė", + "details": "Informacija apie subjektą", + "no-entities-prompt": "Subjektų nėra", + "no-data": "Nėra duomenų atvaizdavimui", + "columns-to-display": "Rodomi stulpeliai", + "type-api-usage-state": "API naudojimo būsena", + "type-edge": "Edge", + "type-edges": "Edge įrenginiai", + "list-of-edges": "{ count, plural, =1 {Vienas Edge} other {# Edge sąrašas} }", + "edge-name-starts-with": "Edge, kurių pavadinimai prasideda '{{prefix}}'", + "version-conflict": { + "message": "Ar norite perrašyti esamą versiją, ar atmesti pakeitimus ir įkelti naujausią versiją?", + "link": "Savo {{entityType}} versiją galite atsisiųsti naudojantis šia", + "overwrite": "Perrašyti versiją", + "discard": "Atmesti pakeitimus" + }, + "type-tb-resource": "Išteklius", + "type-tb-resources": "Ištekliai", + "list-of-tb-resources": "{ count, plural, =1 {Vienas išteklius} other {# išteklių sąrašas} }", + "type-ota-package": "OTA paketas", + "type-ota-packages": "OTA paketai", + "list-of-ota-packages": "{ count, plural, =1 {Vienas OTA paketas} other {# OTA paketų sąrašas} }", + "type-rpc": "RPC", + "type-queue": "Eilė", + "type-queue-stats": "Eilės statistika", + "type-queues-stats": "Eilių statistika", + "type-notification": "Pranešimas", + "type-notification-rule": "Pranešimo taisyklė", + "type-notification-rules": "Pranešimų taisyklės", + "list-of-notification-rules": "{ count, plural, =1 {Viena pranešimo taisyklė} other {# pranešimų taisyklių sąrašas} }", + "type-notification-target": "Pranešimo gavėjas", + "type-notification-targets": "Pranešimų gavėjai", + "list-of-notification-targets": "{ count, plural, =1 {Vienas pranešimo gavėjas} other {# pranešimų gavėjų sąrašas} }", + "type-notification-request": "Pranešimo užklausa", + "type-notification-template": "Pranešimo šablonas", + "type-notification-templates": "Pranešimų šablonai", + "list-of-notification-templates": "{ count, plural, =1 {Vienas pranešimo šablonas} other {# pranešimų šablonų sąrašas} }", + "link": "nuoroda", + "type-oauth2-client": "OAuth 2.0 klientas", + "type-oauth2-clients": "OAuth 2.0 klientai", + "list-of-oauth2-clients": "{ count, plural, =1 {Vienas OAuth 2.0 klientas} other {# OAuth 2.0 klientų sąrašas} }", + "type-domain": "Domenas", + "type-domains": "Domenai", + "list-of-domains": "{ count, plural, =1 {Vienas domenas} other {# domenų sąrašas} }", + "type-mobile-app": "Mobilioji programa", + "type-mobile-apps": "Mobiliosios programos", + "list-of-mobile-apps": "{ count, plural, =1 {Viena mobilioji programa} other {# mobiliųjų programų sąrašas} }", + "type-mobile-app-bundle": "Mobiliųjų programų rinkinys", + "type-mobile-app-bundles": "Mobiliųjų programų rinkiniai", + "list-of-mobile-app-bundles": "{ count, plural, =1 {Vienas mobiliųjų programų rinkinys} other {# mobiliųjų programų rinkinių sąrašas} }" + }, + "entity-field": { + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "type": "Tipas", + "first-name": "Vardas", + "last-name": "Pavardė", + "email": "El. paštas", + "title": "Pavadinimas", + "country": "Šalis", + "state": "Rajonas", + "city": "Miestas", + "address": "Adresas", + "address2": "Adresas 2", + "zip": "Pašto kodas", + "phone": "Telefonas", + "label": "Etiketė", + "queue-name": "Eilės pavadinimas", + "service-id": "Paslaugos ID", + "owner-name": "Savininko vardas", + "owner-type": "Savininko tipas" + }, + "entity-view": { + "entity-view": "Subjektų rodiniai", + "entity-view-required": "Subjeto rodinys būtinas.", + "entity-views": "Subjektų rodiniai", + "management": "Subjektų rodinių valdymas", + "view-entity-views": "Peržiūrėti subjektų rodinius", + "entity-view-alias": "Subjektų rodinio pseudonimas", + "aliases": "Subjektų rodinio pseudonimai", + "no-alias-matching": "'{{alias}}' nerastas.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerastas.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", + "no-entity-views-matching": "Subjektų rodinių, atitinkančių '{{entity}}' nėra.", + "public": "Viešas", + "alias": "Pseudonimas", + "alias-required": "Subjekto rodinio pseudonimas būtinas.", + "remove-alias": "Panaikinti subjekto rodinio pseudonimą", + "add-alias": "Pridėti subjekto rodinio pseudonimą", + "name-starts-with": "Subjekto rodinio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%subjekto_rodinio_pavadinimo_fragmentas%', '%subjekto_rodinio_pavadinimo_pabaiga', 'subjekto_rodinio_pavadinimo_pradžia%'.", + "entity-view-list": "Subjektų rodinių sąrašas", + "use-entity-view-name-filter": "Naudoti filtrą", + "entity-view-list-empty": "Subjektai nepasirinkti.", + "entity-view-name-filter-required": "Subjekto rodinio pavadinimo filtras būtinas.", + "entity-view-name-filter-no-entity-view-matched": "Subjektų rodinių, kurių pavadinimai prasideda '{{entityView}}' nėra.", + "add": "Pridėti subjektų rodinį", + "entity-view-public": "Subjektų rodnys yra viešas", + "assign-to-customer": "Priskirti klientui", + "assign-entity-view-to-customer": "Subjektų rodinį (-ius) priskirti klientui", + "assign-entity-view-to-customer-text": "Pasirinkite subjektų rodinius, kuriuos norite priskirti klientui", + "no-entity-views-text": "Subjektų rodinių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti subjektų rodinį (-ius)", + "entity-view-details": "Informacija apie subjektų rodinį", + "add-entity-view-text": "Pridėti naują subjektų rodinį", + "delete": "Panikinti subjektų rodinį", + "assign-entity-views": "Priskirti subjektų rodinius", + "assign-entity-views-text": "Priskirti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } klientui", + "delete-entity-views": "Panaikinti subjektų rodinius", + "make-public": "Subjektų rodinį padaryti viešu", + "make-private": "Subjektų rodinį padaryti privačiu", + "unassign-from-customer": "Atsieti nuo kliento", + "unassign-entity-views": "Atsieti subjektų rodinius", + "unassign-entity-views-action-title": "Atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } nuo kliento", + "assign-new-entity-view": "Priskirti naują subjektų rodinį", + "delete-entity-view-title": "Ar tikrai norite panaikinti subjektų rodinį '{{entityViewName}}'?", + "delete-entity-view-text": "Būkite dėmesingi, po patvirtinimo, subjektų rodinys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "delete-entity-views-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subketų rodinį} other {# subjektų rodinius} }?", + "delete-entity-views-action-title": "Panaikinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }", + "delete-entity-views-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektų rodiniai ir visa su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "make-public-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti viešu?", + "make-public-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps vieša ir matoma kitiems vartotojams.", + "make-private-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti privačiu?", + "make-private-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps privati ir kiti vartotojai jų nebematys.", + "unassign-entity-view-title": "Ar tikrai norite atsieti subjektų rodinį '{{entityViewName}}'?", + "unassign-entity-view-text": "Po patvirtinimo subjektų rodinys bus atsietas ir klientas jo nebematys.", + "unassign-entity-view": "Atsieti subjektų rodinį", + "unassign-entity-views-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", + "unassign-entity-views-text": "Po patvirtinimo visi pasirinkti subjektų rodiniai bus atsieti ir klientas jų nebematys.", + "entity-view-type": "Subjektų rodinio tipas", + "entity-view-type-required": "Subjektų rodinio tipas būtinas.", + "select-entity-view-type": "Pasirinkite subjektų rodinio tipą", + "enter-entity-view-type": "Įveskite subjektų rodinio tipą", + "any-entity-view": "Bet kuris subjektų tipas", + "no-entity-view-types-matching": "Subjektų rodinių, atitinkančių '{{entitySubtype}}' nėra.", + "entity-view-type-list-empty": "Subjektų tipai nepasirinkti.", + "entity-view-types": "Subjektų rodinio tipai", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "type-max-length": "Rodinio tipas negali viršyti 256 simbolių", + "description": "Aprašymas", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti subjekto rodinio Id", + "idCopiedMessage": "Subjektų rodinio Id nukopjuotas į iškarpinę", + "assignedToCustomer": "Priskirtas klientui", + "unable-entity-view-device-alias-title": "Subjektų rodinio pseudonimo ištrinti nepavyko", + "unable-entity-view-device-alias-text": "Įrenginio pseudonimas '{{entityViewAlias}}' Negali būti ištrintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", + "select-entity-view": "Pasirinkti subjektų rodinį", + "start-ts": "Pradžios laikas", + "end-ts": "Pabaigos laikas", + "date-limits": "Datos limitas", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendrinami atributai", + "server-attributes": "Serverio atributai", + "timeseries": "Telemetrija", + "client-attributes-placeholder": "Kliento atributai", + "shared-attributes-placeholder": "Bendrinami atributai", + "server-attributes-placeholder": "Serverio atributai", + "timeseries-placeholder": "Telemetrija", + "target-entity": "Tikslinis subjektas", + "attributes-propagation": "Atributų paplitimas", + "attributes-propagation-hint": "Subjektų rodinys automatikškai kopijuos nurodytus atributus iš tikslinio subjekto kiekvieną kartą, kai išsaugosite ar atnaujinsite šį rodinį. Norint išsaugoti sistemos spartą, tikslinio subjekto atributai neperkeliami į subjektų rodinį kiekvieną kartą kai jie pasikeičia. Tačiau jūs galite įjungti automatinį atributų reikšmių perdavimą, taisyklių grandinėje sukonfigūravę \"kopijuoti į vaizdą\" taisyklę ir \"Pateikti atributus\" bei \"Atributai atnaujinti\" žinutes nukreipę į naują taisyklę.", + "timeseries-data": "Temetrijos duomenys", + "timeseries-data-hint": "Sukonfigūruokite tikslinio objekto telemetrijos duomenų raktus, kurie bus pasiekiami objekto rodinyje. Šios telemetrijos duomenys yra tik skaitomi.", + "search": "Subjektų rodinių paieška", + "selected-entity-views": "Pasirinkta { count, plural, =1 {1 subjektų rodinys} other {# subjektų rodiniai} }", + "assign-entity-view-to-edge": "Priskirti subjekto rodinį (-ius) „Edge“ sistemai", + "assign-entity-view-to-edge-text": "Pasirinkite subjekto rodinius, kuriuos norite priskirti „Edge“ sistemai", + "unassign-entity-view-from-edge-title": "Ar tikrai norite atsieti subjekto rodinį '{{entityViewName}}' nuo „Edge“?", + "unassign-entity-view-from-edge-text": "Po patvirtinimo subjekto rodinys bus atsietas ir nebebus pasiekiamas per „Edge“.", + "unassign-entity-views-from-edge-action-title": "Atsieti { count, plural, =1 {1 subjekto rodinį} other {# subjekto rodinius} } nuo „Edge“", + "unassign-entity-view-from-edge": "Atsieti subjekto rodinį", + "unassign-entity-views-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjekto rodinį} other {# subjekto rodinius} }?", + "unassign-entity-views-from-edge-text": "Po patvirtinimo visi pasirinkti subjekto rodiniai bus atsieti ir nebebus pasiekiami per „Edge“." + }, + "event": { + "event-type": "Įvykio tipas", + "events-filter": "Įvykių filtras", + "clean-events": "Išvalyti įvykius", + "type-error": "Klaida", + "type-lc-event": "Gyvavimo ciklo įvykis", + "type-stats": "Statistika", + "type-debug-rule-node": "Derinimas (Rule Node)", + "type-debug-rule-chain": "Derinimas (Rule Chain)", + "type-debug-calculated-field": "Derinimas (Calculated Field)", + "arguments": "Argumentai", + "result": "Rezultatas", + "no-events-prompt": "Įvykių nėra", + "error": "Klaida", + "alarm": "Įspėjimas", + "event-time": "Įvykio laikas", + "server": "Serveris", + "body": "Turinys", + "method": "Metodas", + "type": "Tipas", + "metadata": "Metaduomenys", + "message": "Pranešimas", + "message-id": "Pranešimo ID", + "copy-message-id": "Kopijuoti pranešimo ID", + "message-type": "Pranešimo tipas", + "data-type": "Duomenų tipas", + "relation-type": "Ryšio tipas", + "data": "Duomenys", + "event": "Įvykis", + "status": "Būsena", + "success": "Sėkmingas", + "failed": "Nepavyko", + "messages-processed": "Apdoroti pranešimai", + "max-messages-processed": "Maks. apdorotų pranešimų", + "min-messages-processed": "Min. apdorotų pranešimų", + "errors-occurred": "Įvyko klaidų", + "max-errors-occurred": "Maks. klaidų", + "min-errors-occurred": "Min. klaidų", + "min-value": "Mažiausia leistina reikšmė yra 0.", + "all-events": "Visi", + "has-error": "Turi klaidą", + "entity-id": "Subjekto ID", + "copy-entity-id": "Kopijuoti subjekto ID", + "entity-type": "Subjekto tipas", + "clear-filter": "Išvalyti filtrą", + "clear-request-title": "Išvalyti visus įvykius", + "clear-request-text": "Ar tikrai norite išvalyti visus įvykius?", + "started": "Pradėta", + "updated": "Atnaujinta", + "stopped": "Sustabdyta" + }, + "extension": { + "extensions": "Plėtiniai", + "selected-extensions": "Pasirinkta { count, plural, =1 {1 plėtinys} other {# plėtiniai} }", + "type": "Tipas", + "key": "Raktas", + "value": "Reikšmė", + "id": "Id", + "extension-id": "Plėtinio ID", + "extension-type": "Plėtinio tipas", + "transformer-json": "JSON *", + "unique-id-required": "Dabartinis plėtinio ID jau egzistuoja.", + "delete": "Pašalinti plėtinį", + "add": "Pridėti plėtinį", + "edit": "Redaguoti plėtinį", + "delete-extension-title": "Ar tikrai norite pašalinti plėtinį '{{extensionId}}'?", + "delete-extension-text": "Būkite atsargūs — po patvirtinimo šis plėtinys ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-extensions-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 plėtinį} other {# plėtinius} }?", + "delete-extensions-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti plėtiniai bus pašalinti.", + "converters": "Keitikliai", + "converter-id": "Keitiklio ID", + "configuration": "Konfigūracija", + "converter-configurations": "Keitiklio konfigūracijos", + "token": "Saugos raktas", + "add-converter": "Pridėti keitiklį", + "add-config": "Pridėti keitiklio konfigūraciją", + "device-name-expression": "Įrenginio pavadinimo išraiška", + "device-type-expression": "Įrenginio tipo išraiška", + "custom": "Tinkintas", + "to-double": "Konvertuoti į Double", + "transformer": "Transformatorius", + "json-required": "Reikalingas transformatoriaus JSON.", + "json-parse": "Nepavyko apdoroti transformatoriaus JSON.", + "attributes": "Atributai", + "add-attribute": "Pridėti atributą", + "add-map": "Pridėti susiejimo elementą", + "timeseries": "Telemetrija", + "add-timeseries": "Pridėti telemetriją", + "field-required": "Laukas būtinas.", + "brokers": "Tarpiniai serveriai", + "add-broker": "Pridėti tarpinį serverį", + "host": "Serveris", + "port": "Prievadas", + "port-range": "Prievadas turi būti tarp 1 ir 65535.", + "ssl": "SSL", + "credentials": "Įgaliojimai", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "retry-interval": "Kartojimo intervalas (ms)", + "anonymous": "Anoniminis", + "basic": "Paprastas (Basic)", + "pem": "PEM", + "ca-cert": "CA sertifikato failas *", + "private-key": "Privataus rakto failas *", + "cert": "Sertifikato failas *", + "no-file": "Failas nepasirinktas.", + "drop-file": "Įmeskite failą arba spustelėkite, kad pasirinktumėte įkėlimui.", + "mapping": "Susiejimas", + "topic-filter": "Temos filtras", + "converter-type": "Keitiklio tipas", + "converter-json": "JSON", + "json-name-expression": "Įrenginio pavadinimo JSON išraiška", + "topic-name-expression": "Įrenginio pavadinimo temos išraiška", + "json-type-expression": "Įrenginio tipo JSON išraiška", + "topic-type-expression": "Įrenginio tipo temos išraiška", + "attribute-key-expression": "Atributo rakto išraiška", + "attr-json-key-expression": "Atributo rakto JSON išraiška", + "attr-topic-key-expression": "Atributo rakto temos išraiška", + "request-id-expression": "Užklausos ID išraiška", + "request-id-json-expression": "Užklausos ID JSON išraiška", + "request-id-topic-expression": "Užklausos ID temos išraiška", + "response-topic-expression": "Atsakymo temos išraiška", + "value-expression": "Reikšmės išraiška", + "topic": "Tema", + "timeout": "Laiko limitas (ms)", + "converter-json-required": "Keitiklio JSON yra būtinas.", + "converter-json-parse": "Nepavyko apdoroti keitiklio JSON.", + "filter-expression": "Filtro išraiška", + "connect-requests": "Prisijungimo užklausos", + "add-connect-request": "Pridėti prisijungimo užklausą", + "disconnect-requests": "Atsijungimo užklausos", + "add-disconnect-request": "Pridėti atsijungimo užklausą", + "attribute-requests": "Atributų užklausos", + "add-attribute-request": "Pridėti atributo užklausą", + "attribute-updates": "Atributų atnaujinimai", + "add-attribute-update": "Pridėti atributo atnaujinimą", + "server-side-rpc": "Serverio pusės RPC", + "add-server-side-rpc-request": "Pridėti serverio pusės RPC užklausą", + "device-name-filter": "Įrenginio pavadinimo filtras", + "attribute-filter": "Atributų filtras", + "method-filter": "Metodo filtras", + "request-topic-expression": "Užklausos temos išraiška", + "response-timeout": "Atsakymo laukimo limitas (ms)", + "topic-expression": "Temos išraiška", + "client-scope": "Kliento sritis", + "add-device": "Pridėti įrenginį", + "opc-server": "Serveriai", + "opc-add-server": "Pridėti serverį", + "opc-add-server-prompt": "Prašome pridėti serverį", + "opc-application-name": "Programos pavadinimas", + "opc-application-uri": "Programos URI", + "opc-scan-period-in-seconds": "Skenavimo periodas (s)", + "opc-security": "Saugumas", + "opc-identity": "Tapatybė", + "opc-keystore": "Raktinė (Keystore)", + "opc-type": "Tipas", + "opc-keystore-type": "Raktinės tipas", + "opc-keystore-location": "Vieta *", + "opc-keystore-password": "Slaptažodis", + "opc-keystore-alias": "Aliasas", + "opc-keystore-key-password": "Rakto slaptažodis", + "opc-device-node-pattern": "Įrenginio mazgo šablonas", + "opc-device-name-pattern": "Įrenginio pavadinimo šablonas", + "modbus-server": "Serveriai / vergai", + "modbus-add-server": "Pridėti serverį / vergą", + "modbus-add-server-prompt": "Prašome pridėti serverį / vergą", + "modbus-transport": "Transportas", + "modbus-tcp-reconnect": "Automatiškai prisijungti iš naujo", + "modbus-rtu-over-tcp": "RTU per TCP", + "modbus-port-name": "Serijinio prievado pavadinimas", + "modbus-encoding": "Kodavimas", + "modbus-parity": "Paritetas", + "modbus-baudrate": "Perdavimo greitis (baud)", + "modbus-databits": "Duomenų bitai", + "modbus-stopbits": "Stop bitai", + "modbus-databits-range": "Duomenų bitai turi būti nuo 7 iki 8.", + "modbus-stopbits-range": "Stop bitai turi būti nuo 1 iki 2.", + "modbus-unit-id": "Vieneto ID", + "modbus-unit-id-range": "Vieneto ID turi būti nuo 1 iki 247.", + "modbus-device-name": "Įrenginio pavadinimas", + "modbus-poll-period": "Užklausos periodas (ms)", + "modbus-attributes-poll-period": "Atributų užklausos periodas (ms)", + "modbus-timeseries-poll-period": "Telemetrijos užklausos periodas (ms)", + "modbus-poll-period-range": "Užklausos periodas turi būti teigiama reikšmė.", + "modbus-tag": "Žyma", + "modbus-function": "Funkcija", + "modbus-register-address": "Registrų adresas", + "modbus-register-address-range": "Registrų adresas turi būti nuo 0 iki 65535.", + "modbus-register-bit-index": "Bito indeksas", + "modbus-register-bit-index-range": "Bito indeksas turi būti nuo 0 iki 15.", + "modbus-register-count": "Registrų skaičius", + "modbus-register-count-range": "Registrų skaičius turi būti teigiama reikšmė.", + "modbus-byte-order": "Baitų tvarka", + "sync": { + "status": "Būsena", + "sync": "Sinchronizuota", + "not-sync": "Nesinchronizuota", + "last-sync-time": "Paskutinio sinchronizavimo laikas", + "not-available": "Nėra duomenų" + }, + "export-extensions-configuration": "Eksportuoti plėtinių konfigūraciją", + "import-extensions-configuration": "Importuoti plėtinių konfigūraciją", + "import-extensions": "Importuoti plėtinius", + "import-extension": "Importuoti plėtinį", + "export-extension": "Eksportuoti plėtinį", + "file": "Plėtinių failas", + "invalid-file-error": "Neteisingas plėtinio failas" + }, + "feature": { + "advanced-features": "Išplėstinės funkcijos" + }, + "filter": { + "add": "Pridėti filtrą", + "edit": "Redaguoti filtrą", + "name": "Filtro pavadinimas", + "name-required": "Filtro pavadinimas būtinas.", + "duplicate-filter": "Toks filtro pavadinimas jau egzistuoja.", + "filters": "Filtrai", + "unable-delete-filter-title": "Nepavyko ištrinti filtro", + "unable-delete-filter-text": "Filtras '{{filter}}' negali būti ištrintas, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "duplicate-filter-error": "Rastas pasikartojantis filtras '{{filter}}'.
    Filtrų pavadinimai skydelyje turi būti unikalūs.", + "missing-key-filters-error": "Trūksta pagrindinio filtro '{{filter}}'.", + "filter": "Filtras", + "editable": "Redaguojamas", + "editable-hint": "Leisti vartotojui pakeisti filtro reikšmę skydelyje.", + "no-filters-found": "Filtrų nerasta.", + "no-filter-text": "Filtras nenurodytas", + "add-filter-prompt": "Pridėkite filtrą", + "no-filter-matching": "Filtras '{{filter}}' nerastas.", + "create-new-filter": "Sukurti naują!", + "create-new": "Sukurti naują", + "filter-required": "Filtras būtinas.", + "operation": { + "operation": "Operacija", + "equal": "lygu", + "not-equal": "nelygu", + "starts-with": "prasideda nuo", + "ends-with": "baigiasi", + "contains": "turi reikšmę", + "not-contains": "neturi reikšmės", + "greater": "didesnis nei", + "less": "mažesnis nei", + "greater-or-equal": "didesnis arba lygus", + "less-or-equal": "mažesnis arba lygus", + "and": "ir", + "or": "arba", + "in": "yra sąraše", + "not-in": "nėra sąraše" + }, + "ignore-case": "Nepaisyti raidžių dydžio", + "value": "Reikšmė", + "remove-filter": "Pašalinti filtrą", + "duplicate-filter-action": "Dubliuoti filtrą", + "preview": "Filtro peržiūra", + "no-filters": "Filtrai nesukonfigūruoti", + "add-filter": "Pridėti filtrą", + "add-complex-filter": "Pridėti sudėtingą filtrą", + "add-complex": "Pridėti sudėtingą", + "complex-filter": "Sudėtingas filtras", + "edit-complex-filter": "Redaguoti sudėtingą filtrą", + "edit-filter-user-params": "Redaguoti filtro naudotojo parametrus", + "filter-user-params": "Filtro naudotojo parametrai", + "user-parameters": "Naudotojo parametrai", + "display-label": "Rodoma etiketė", + "custom-label": "Pasirinktinė etiketė", + "custom-label-hint": "Įjunkite, jei norite nustatyti savo etiketę filtrui. Išjungus, etiketė bus sugeneruota automatiškai.", + "order-priority": "Lauko tvarkos prioritetas", + "key-filter": "Raktinis filtras", + "key-filters": "Raktiniai filtrai", + "key-name": "Rakto pavadinimas", + "key-name-required": "Rakto pavadinimas būtinas.", + "key-type": { + "key-type": "Rakto tipas", + "attribute": "Atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas", + "constant": "Konstanta", + "client-attribute": "Kliento atributas", + "server-attribute": "Serverio atributas", + "shared-attribute": "Bendrinamas atributas" + }, + "value-type": { + "value-type": "Reikšmės tipas", + "string": "Tekstas", + "numeric": "Skaitinė", + "boolean": "Loginė (taip/ne)", + "date-time": "Data/laikas" + }, + "value-type-required": "Rakto reikšmės tipas būtinas.", + "key-value-type-change-title": "Ar tikrai norite pakeisti rakto reikšmės tipą?", + "key-value-type-change-message": "Patvirtinus naują reikšmės tipą, visi įvesti rakto filtrai bus pašalinti.", + "no-key-filters": "Rakto filtrai nesukonfigūruoti", + "add-key-filter": "Pridėti rakto filtrą", + "remove-key-filter": "Pašalinti rakto filtrą", + "edit-key-filter": "Redaguoti rakto filtrą", + "date": "Data", + "time": "Laikas", + "current-tenant": "Dabartinis valdytojas", + "current-customer": "Dabartinis klientas", + "current-user": "Dabartinis vartotojas", + "current-device": "Dabartinis įrenginys", + "default-value": "Numatytoji reikšmė", + "default-comma-separated-values": "Numatytosios reikšmės, atskirtos kableliais", + "dynamic-source-type": "Dinaminio šaltinio tipas", + "dynamic-value": "Dinaminė reikšmė", + "no-dynamic-value": "Nėra dinaminės reikšmės", + "source-attribute": "Šaltinio atributas", + "switch-to-dynamic-value": "Perjungti į dinaminę reikšmę", + "switch-to-default-value": "Perjungti į numatytąją reikšmę", + "inherit-owner": "Paveldėti iš savininko", + "source-attribute-not-set": "Jei šaltinio atributas nenustatytas", + "unit": "Vienetas" + }, + "fullscreen": { + "expand": "Rodyti per visą ekraną", + "exit": "Išjungti rodymą per visą ekraną", + "toggle": "Rodyti per visą ekraną", + "fullscreen": "Rodyti per visą ekraną" + }, + "function": { + "function": "Funkcija" + }, + "gateway": { + "gateway-name": "Šliuzo pavadinimas", + "gateway-name-required": "Šliuzo pavadinimas būtinas.", + "gateways": "Šliuzai", + "create-new-gateway": "Sukurti naują šliuzą", + "create-new-gateway-text": "Ar tikrai norite sukurti naują šliuzą pavadinimu: '{{gatewayName}}'?", + "launch-command": "Paleidimo komanda", + "no-gateway-found": "Šliuzų nerasta.", + "no-gateway-matching": "'{{item}}' nerasta." + }, + "grid": { + "delete-item-title": "Ar tikrai norite pašalinti šį elementą?", + "delete-item-text": "Būkite dėmesingi, po patvirtinimo šio elemento ir visos su juo susijusios informacijos atkurti nebegalėsite.", + "delete-items-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 elementą} other {# elementus} }?", + "delete-items-action-title": "Panaikinti { count, plural, =1 {1 elementą} other {# elementus} }", + "delete-items-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti elementai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "add-item-text": "Pridėti naują elementą", + "no-items-text": "Elementų nėra", + "item-details": "Informacija apie elementą", + "delete-item": "Panaikinti elementą", + "delete-items": "Panaikinti elmentus", + "scroll-to-top": "Slinkti į viršų" + }, + "help": { + "goto-help-page": "Pagalba", + "show-help": "Pagalba" + }, + "home": { + "home": "Pagrindinis", + "profile": "Profilis", + "logout": "Atsijungti", + "menu": "Meniu", + "avatar": "Paveiksliukas", + "open-user-menu": "Atverti vartoto meniu" + }, + "file-input": { + "browse-file": "Pasirinkti failą", + "browse-files": "Pasirinkti failus" + }, + "image": { + "gallery": "Paveikslėlių galerija", + "search": "Ieškoti paveikslėlio", + "selected-images": "{ count, plural, =1 {1 paveikslėlis} other {# paveikslėliai} } pasirinkta", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "resolution": "Raiška", + "size": "Dydis", + "system": "Sistema", + "download-image": "Atsisiųsti paveikslėlį", + "export-image": "Eksportuoti paveikslėlį į JSON", + "import-image": "Importuoti paveikslėlį iš JSON", + "upload-image": "Įkelti paveikslėlį", + "edit-image": "Redaguoti paveikslėlį", + "image-details": "Paveikslėlio informacija", + "no-images": "Paveikslėlių nerasta", + "delete-image": "Pašalinti paveikslėlį", + "delete-image-title": "Ar tikrai norite pašalinti paveikslėlį '{{imageTitle}}'?", + "delete-image-text": "Būkite dėmesingi — po patvirtinimo paveikslėlio atkurti nebus įmanoma.", + "delete-images-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 paveikslėlį} other {# paveikslėlius} }?", + "delete-images-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti paveikslėliai bus pašalinti, o susiję duomenys taps negrąžinami.", + "list-mode": "Sąrašo rodinys", + "grid-mode": "Tinklelio rodinys", + "image-preview": "Paveikslėlio peržiūra", + "update-image": "Atnaujinti paveikslėlį", + "export-failed-error": "Nepavyko eksportuoti paveikslėlio: {{error}}", + "image-json-file": "Paveikslėlio JSON failas", + "invalid-image-json-file-error": "Nepavyko importuoti paveikslėlio iš JSON: neteisinga JSON duomenų struktūra.", + "image-is-in-use": "Paveikslėlis naudojamas kituose subjektuose", + "images-are-in-use": "Paveikslėliai naudojami kituose subjektuose", + "image-is-in-use-text": "Paveikslėlis '{{title}}' nebuvo ištrintas, nes jis naudojamas šiuose subjektuose:", + "images-are-in-use-text": "Ne visi paveikslėliai buvo ištrinti, nes jie naudojami kituose subjektuose.
    Galite peržiūrėti susijusius subjektus spustelėję Nuorodos mygtuką atitinkamoje eilutėje.
    Jei vis tiek norite ištrinti šiuos paveikslėlius, pasirinkite juos ir spustelėkite Ištrinti pasirinktus.", + "delete-image-in-use-text": "Jei vis tiek norite ištrinti paveikslėlį, spustelėkite Ištrinti vis tiek.", + "system-entities": "Sistemos subjektai:", + "entities": "Subjektai:", + "references": "Nuorodos", + "include-system-images": "Įtraukti sistemos paveikslėlius", + "clear-image": "Išvalyti paveikslėlį", + "no-image": "Nėra paveikslėlio", + "no-image-selected": "Nepasirinktas paveikslėlis", + "browse-from-gallery": "Pasirinkti iš galerijos", + "set-link": "Nustatyti nuorodą", + "image-link": "Paveikslėlio nuoroda", + "link": "Nuoroda", + "copy-image-link": "Kopijuoti paveikslėlio nuorodą", + "embed-image": "Įterpti paveikslėlį", + "embed-to-html": "Įterpti į HTML", + "embed-to-html-hint": "Ši funkcija leis nuorodą pasiekti neautorizuotiems naudotojams.", + "embed-to-html-text": "Naudodami šį kodo fragmentą galite įterpti paveikslėlį į HTML pagrįstus komponentus.
    Tokie komponentai apima HTML kortelių valdiklius, langelių turinio funkcijas ir pan.", + "embed-to-angular-template": "Įterpti į Angular HTML šabloną", + "embed-to-angular-template-text": "Naudodami šį kodo fragmentą galite įterpti paveikslėlį į Angular HTML šabloną, naudojamą komponentams.
    Tokie komponentai apima Markdown valdiklį, HTML sekciją valdiklio redaktoriuje, pasirinktus veiksmus ir pan." + }, + "image-input": { + "drop-images-or": "Užvilkite paveikslėlį arba", + "drag-and-drop": "Užvilkite", + "or": "arba", + "browse": "Pasirinkite", + "no-images": "Paveikslėlis nepasirinktas", + "images": "paveikslėliai" + }, + "import": { + "no-file": "Nepasirinktas failas", + "drop-file": "Nuvilkite JSON failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-json-file-or": "Nuvilkite JSON failą arba", + "drop-file-csv": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-file-csv-or": "Nuvilkite CSV failą arba", + "column-value": "Reikšmė", + "column-title": "Pavadinimas", + "column-example": "Duomenų pavyzdys", + "column-key": "Atributo/telemetrijos raktas", + "credentials": "Įgaliojimai", + "csv-delimiter": "CSV skyriklis", + "csv-first-line-header": "Pirmoje eilutėje stulpelių pavadinimai", + "csv-update-data": "Atnaujinti artibutą/telemetriją", + "details": "Informacija", + "import-csv-number-columns-error": "Faile turi būti bent du stulpeliai", + "import-csv-invalid-format-error": "Neteisingas failo formatas. Elutė: '{{line}}'", + "column-type": { + "name": "Pavadinimas", + "type": "Tipas", + "label": "Etiketė", + "column-type": "Stulpelio tipas", + "client-attribute": "Kliento atributas", + "shared-attribute": "Bendrinamas atributas", + "server-attribute": "Serverio atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas", + "access-token": "Prieigos raktas", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT kliento ID", + "user-name": "MQTT vartotojo vardas", + "password": "MQTT slaptažodis" + }, + "lwm2m": { + "client-endpoint": "LwM2M kliento galinio taško pavadinimas", + "security-config-mode": "LwM2M saugumo konfigūracijos režimas", + "client-identity": "LwM2M kliento identifikatorius", + "client-key": "LwM2M kliento raktas", + "client-cert": "LwM2M kliento viešasis raktas", + "bootstrap-server-security-mode": "LwM2M pradinio serverio (bootstrap) saugumo režimas", + "bootstrap-server-secret-key": "LwM2M pradinio serverio slaptasis raktas", + "bootstrap-server-public-key-id": "LwM2M pradinio serverio viešasis raktas arba ID", + "lwm2m-server-security-mode": "LwM2M serverio saugumo režimas", + "lwm2m-server-secret-key": "LwM2M serverio slaptasis raktas", + "lwm2m-server-public-key-id": "LwM2M serverio viešasis raktas arba ID" + }, + "snmp": { + "host": "SNMP serveris (hostas)", + "port": "SNMP prievadas (portas)", + "version": "SNMP versija (v1, v2c arba v3)", + "community-string": "SNMP bendruomenės eilutė" + }, + "isgateway": "Yra šliuzas", + "activity-time-from-gateway-device": "Aktyvumo laikas iš šliuzo įrenginio", + "description": "Aprašymas", + "routing-key": "Šliuzo raktas", + "secret": "Šliuzo slaptas raktas" + }, + "stepper-text": { + "select-file": "Pasirinkite failą", + "configuration": "Importo konfigūracija", + "column-type": "Pasirinkite stulpelių tipus", + "creat-entities": "Sukurti naujus subjektus" + }, + "message": { + "create-entities": "Sukurti {{count}} nauji subjektai.", + "update-entities": "Atnaujinti {{count}} subjektai.", + "error-entities": "Kuriant {{count}} subjektų įvyko klaidų." + } + }, + "scada": { + "symbols": "SCADA simboliai", + "search": "Ieškoti simbolio", + "selected-symbols": "{ count, plural, =1 {1 simbolis} other {# simboliai} } pasirinkta", + "download-symbol": "Atsisiųsti SCADA simbolį", + "export-symbol": "Eksportuoti SCADA simbolį į JSON", + "import-symbol": "Importuoti SCADA simbolį iš JSON", + "upload-symbol": "Įkelti SCADA simbolį", + "update-symbol": "Atnaujinti SCADA simbolį", + "edit-symbol": "Redaguoti SCADA simbolį", + "symbol-details": "SCADA simbolio informacija", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "Simbolių nerasta", + "show-hidden-elements": "Rodyti paslėptus elementus", + "hide-hidden-elements": "Slėpti paslėptus elementus", + "delete-symbol": "Ištrinti SCADA simbolį", + "delete-symbol-title": "Ar tikrai norite ištrinti SCADA simbolį '{{imageTitle}}'?", + "delete-symbol-text": "Būkite atsargūs — po patvirtinimo SCADA simbolis bus negrįžtamai pašalintas.", + "delete-symbols-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 SCADA simbolį} other {# SCADA simbolius} }?", + "delete-symbols-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti SCADA simboliai ir susiję duomenys bus negrįžtamai pašalinti.", + "include-system-symbols": "Įtraukti sistemos simbolius", + "symbol-preview": "Simbolio peržiūra", + "general": "Bendra informacija", + "tags": "Žymos", + "properties": "Savybės", + "title": "Pavadinimas", + "description": "Aprašymas", + "search-tags": "Ieškoti žymų", + "widget-size": "Valdiklio dydis", + "cols": "stulpeliai", + "rows": "eilutės", + "state-render-function": "Būsenos atvaizdavimo funkcija", + "preview": "Peržiūra", + "preview-widget-action-text": "Valdiklio veiksmas '{{type}}' sėkmingai įvykdytas!", + "no-symbol": "Nėra SCADA simbolio", + "no-symbol-selected": "Nepasirinktas SCADA simbolis", + "clear-symbol": "Išvalyti SCADA simbolį", + "browse-symbol-from-gallery": "Pasirinkti SCADA simbolį iš galerijos", + "zoom-in": "Priartinti", + "zoom-out": "Atitolinti", + "create-widget": "Sukurti valdiklį", + "create-widget-from-symbol": "Sukurti valdiklį iš SCADA simbolio", + "hidden": "paslėpta", + "tag": { + "tag": "Žyma", + "on-click-action": "Veiksmas paspaudus", + "no-tags": "Žymos nesukonfigūruotos", + "delete-tag-text": "Ar tikrai norite pašalinti žymą
    {{tag}} iš {{elementType}} elemento?", + "update-tag": "Atnaujinti žymą", + "enter-tag": "Įveskite žymą", + "tag-settings": "Žymos nustatymai", + "remove-tag": "Pašalinti žymą", + "add-tag": "Pridėti žymą" + }, + "behavior": { + "behavior": "Elgsena", + "id": "ID", + "name": "Pavadinimas", + "type": "Tipas", + "no-behaviors": "Elgsenos nesukonfigūruotos", + "add-behavior": "Pridėti elgseną", + "type-action": "Veiksmas", + "type-value": "Reikšmė", + "type-widget-action": "Valdiklio veiksmas", + "behavior-settings": "Elgsenos nustatymai", + "remove-behavior": "Pašalinti elgseną", + "hint": "Patarimas", + "group-title": "Grupės pavadinimas", + "value-type": "Reikšmės tipas", + "default-value": "Numatytoji reikšmė", + "true-label": "Tiesa etiketė", + "false-label": "Netiesa etiketė", + "state-label": "Būsenos etiketė", + "default-payload": "Numatytasis krovinys", + "not-unique-behavior-ids-error": "Elgsenos ID turi būti unikalūs!", + "default-settings": "Numatytieji nustatymai" + }, + "symbol": { + "symbol": "SCADA simbolis", + "fluid-presence": "Skysčio buvimas", + "fluid-presence-hint": "Nurodo, ar vamzdyje yra skysčio.", + "fluid-present": "Skystis yra", + "present": "Yra", + "absent": "Nėra", + "flow-presence": "Srauto buvimas", + "flow-presence-hint": "Nurodo, ar vamzdyje teka skystis.", + "flow-present": "Srautas yra", + "flow-direction": "Srauto kryptis", + "flow-direction-hint": "Nurodo skysčio tekėjimo kryptį.", + "forward": "Pirmyn", + "reverse": "Atgal", + "flow-animation-speed": "Srauto animacijos greitis", + "flow-animation-speed-hint": "Reikšmė, nurodanti animacijos greitį. 1 – normalus greitis, 0 – be animacijos, < 1 – lėtesnė, > 1 – greitesnė animacija.", + "leak": "Nuotėkis", + "leak-hint": "Nurodo, ar yra nuotėkis vamzdyje.", + "leak-present": "Nuotėkis yra", + "fluid-color": "Skysčio spalva", + "pipe-color": "Vamzdžio spalva", + "horizontal-pipe": "Horizontalus vamzdis", + "vertical-pipe": "Vertikalus vamzdis", + "horizontal-fluid-color": "Horizontalaus skysčio spalva", + "vertical-fluid-color": "Vertikalaus skysčio spalva", + "left-pipe": "Kairysis vamzdis", + "right-pipe": "Dešinysis vamzdis", + "top-pipe": "Viršutinis vamzdis", + "bottom-pipe": "Apatinis vamzdis", + "left-fluid-color": "Kairiojo skysčio spalva", + "right-fluid-color": "Dešiniojo skysčio spalva", + "top-fluid-color": "Viršutinio skysčio spalva", + "bottom-fluid-color": "Apatinio skysčio spalva", + "display": "Ekranas", + "display-format": "Ekrano formatas", + "value": "Reikšmė", + "decimals": "Skaitmenys po kablelio", + "units": "Vienetai", + "flow-meter-value-hint": "Reikšmė rodoma srauto matuoklyje", + "value-hint": "Reikšmė, nurodanti dabartinį dydį", + "running": "Veikia", + "running-hint": "Nurodo, ar komponentas veikia.", + "warning-state": "Įspėjimo būsena", + "warning": "Įspėjimas", + "warning-click": "Įspėjimo paspaudimas", + "warning-state-hint": "Nurodo, ar komponentas yra įspėjimo būsenoje.", + "critical-state": "Kritinė būsena", + "critical": "Kritinis", + "critical-click": "Kritinio paspaudimas", + "critical-state-hint": "Nurodo, ar komponentas yra kritinėje būsenoje.", + "critical-state-animation": "Kritinės būsenos animacija", + "critical-state-animation-hint": "Įjungia mirksėjimo animaciją, kai komponentas yra kritinėje būsenoje.", + "warning-critical-state-animation": "Įspėjimo/kritinės būsenos animacija", + "warning-critical-state-animation-hint": "Įjungia mirksėjimo animaciją, kai komponentas yra įspėjimo ar kritinėje būsenoje.", + "animation": "Animacija", + "broken": "Sugedęs", + "broken-hint": "Nurodo, ar komponentas sugedęs.", + "on-display-click": "Paspaudus ekraną", + "on-display-click-hint": "Veiksmas, atliekamas paspaudus ekraną.", + "pipe": "Vamzdis", + "default-border-color": "Numatytoji rėmelio spalva", + "active-border-color": "Aktyvaus rėmelio spalva", + "warning-border-color": "Įspėjimo rėmelio spalva", + "critical-border-color": "Kritinio rėmelio spalva", + "background-color": "Fono spalva", + "rotation-animation-speed": "Sukimosi animacijos greitis", + "rotation-animation-speed-hint": "Reikšmė, nurodanti sukimosi animacijos greitį. 1 – normalus, 0 – be animacijos, < 1 – lėtesnė, > 1 – greitesnė animacija.", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, atliekamas paspaudus komponentą.", + "connectors-positions": "Jungčių pozicijos", + "right-connector": "Dešinė jungtis", + "right-top-connector": "Dešinysis viršutinis jungtuvas.", + "right-bottom-connector": "Dešinysis apatinis jungtuvas.", + "left-connector": "Kairė jungtis", + "left-top-connector": "Kairysis viršutinis jungtuvas.", + "left-bottom-connector": "Kairysis apatinis jungtuvas.", + "top-left-connector": "Kairysis viršutinis jungtuvas.", + "top-right-connector": "Dešinysis viršutinis jungtuvas.", + "top-connector": "Viršutinė jungtis", + "bottom-connector": "Apatinė jungtis", + "running-color": "Veikimo spalva", + "stopped-color": "Sustojimo spalva", + "stopped": "Sustojęs", + "warning-color": "Įspėjimo spalva", + "critical-color": "Kritinė spalva", + "opened": "Atidaryta", + "opened-hint": "Nurodo, ar komponentas atidarytas.", + "open": "Atidaryti", + "open-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atidarytų komponentą.", + "close": "Uždaryti", + "close-hint": "Veiksmas, atliekamas vartotojui spustelėjus komponento uždarymą.", + "close-state-animation": "Uždarytos būsenos animacija.", + "close-state-animation-hint": "Ar įjungti mirksinčią animaciją, kai komponentas yra uždarytos būsenos.", + "opened-color": "Atidarytos būsenos spalva.", + "closed-color": "Uždarytos būsenos spalva.", + "opened-rotation-angle": "Atidarytos būsenos pasukimo kampas.", + "closed-rotation-angle": "Uždarytos būsenos pasukimo kampas.", + "tank-capacity": "Talpyklos talpa", + "tank-capacity-hint": "Slankiojo kablelio reikšmė, nurodanti bendrą bako talpą.", + "current-volume": "Dabartinis tūris", + "current-volume-hint": "Slankiojo kablelio reikšmė, nurodanti šiuo metu užimtą tūrį.", + "tank-color": "Talpyklos spalva", + "value-box": "Reikšmės langelis.", + "value-text": "Reikšmės tekstas.", + "scale": "Skalė", + "transparent-mode": "Permatomas režimas.", + "major-ticks": "Pagrindiniai žymekliai.", + "intervals": "Intervalai.", + "major-ticks-color": "Pagrindinių žymeklių spalva.", + "normal": "Normalu", + "minor-ticks": "Smulkieji žymekliai.", + "minor-ticks-color": "Smulkiųjų žymeklių spalva.", + "temperature": "Temperatūra", + "temperature-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinę temperatūrą.", + "update-temperature": "Atnaujinti temperatūrą.", + "update-temperature-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad pakeistų dabartinę temperatūrą.", + "run": "Paleisti", + "run-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad paleistų komponentą.", + "stop": "Sustabdyti", + "stop-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad sustabdytų komponentą.", + "temperature-step": "Temperatūros žingsnio padidinimas.", + "heat-pump-color": "Šilumos siurblio spalva.", + "power-button-background": "Maitinimo mygtuko fonas.", + "value-box-background": "Value box background", + "value-units": "Reikšmės vienetai.", + "enable-units-scale": "Įjungti vienetus skalėje.", + "filtration-mode": "Filtravimo režimas.", + "filtration-mode-hint": "Sveikasis skaičius, nurodantis dabartinį filtravimo režimą.", + "filtration-mode-update": "Filtravimo režimo atnaujinimo būsena.", + "filtration-mode-update-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad pakeistų dabartinį filtravimo režimą.", + "filter-mode": "Filtras.", + "waste-mode": "Atliekos.", + "backwash-mode": "Atbulinis plovimas.", + "recirculate-mode": "Recirkuliacija.", + "rinse-mode": "Skalavimas.", + "closed-mode": "Uždaryta.", + "sand-filter-color": "Smėlio filtro spalva.", + "mode-box-background": "Režimo langelio fonas.", + "border-color": "Kraštinės spalva.", + "label-color": "Etiketės spalva", + "water-leak-hint": "Nurodo, ar yra nutekėjimas.", + "default-color": "Numatytoji spalva.", + "leak-color": "Nutekėjimo spalva.", + "full-value": "Pilna reikšmė.", + "full-value-hint": "Slankiojo kablelio reikšmė, nurodanti pilną reikšmę.", + "label": "Etiketė", + "icon": "Ikona", + "button-color": "Mygtuko spalva.", + "on-label": "„Įjungta“ žymės tekstas.", + "off-label": "„Išjungta“ žymės tekstas.", + "arrow-presence": "Rodyklės buvimas.", + "arrow-presence-hint": "Nurodo, ar jungtyje yra rodyklė.", + "arrow-present": "Rodyklė yra.", + "arrow-direction": "Srauto kryptis.", + "arrow-direction-hint": "Nurodo srauto kryptį.", + "flow-animation": "Srauto buvimas.", + "flow-animation-hint": "Nurodo, ar jungtyje teka skystis.", + "flow": "Srautas.", + "flow-line": "Linija.", + "flow-line-style": "Linijos stilius.", + "flow-style-hint": "Nustatykite brūkšnio ir tarpo reikšmes taip, kad jų suma būtų daloma iš 100 be liekanos, kad animacija būtų idealiai sinchronizuota.", + "flow-dash-cap": "Brūkšnio galas.", + "dash-cap-butt": "Galinė briauna.", + "dash-cap-round": "Apvalus.", + "dash-cap-square": "Kvadratinis.", + "dash": "Brūkšnys.", + "gap": "Tarpas.", + "main-line": "Pagrindinė linija.", + "line": "Linija", + "line-color": "Linijos spalva", + "arrow-color": "Rodyklės spalva.", + "target-value": "Tikslo reikšmė.", + "target-value-hint": "Nurodo tikslo tašką skalėje.", + "min-max-value": "Minimali ir maksimali reikšmė.", + "min-value": "Min.", + "max-value": "Maks.", + "progress-bar": "Eigos juosta.", + "progress-arrow": "Eigos rodyklė.", + "warning-scale-color": "Įspėjimo skalės spalva.", + "critical-scale-color": "Kritinės skalės spalva.", + "scale-color": "Skalės spalva.", + "target": "Tikslas.", + "high-warning-state": "Aukšto įspėjimo būsena.", + "show-high-warning-scale": "Rodyti aukšto įspėjimo skalės reikšmę.", + "high-warning-scale": "Aukšto įspėjimo skalė.", + "high-warning-state-hint": "Slankiojo kablelio reikšmė, nurodanti aukšto įspėjimo diapazoną iki aukštos kritinės arba didžiausios reikšmės.", + "low-warning-state": "Žemo įspėjimo būsena.", + "show-low-warning-scale": "Rodyti žemo įspėjimo skalės reikšmę.", + "low-warning-scale": "Žemo įspėjimo skalė.", + "low-warning-state-hint": "Slankiojo kablelio reikšmė, nurodanti žemo įspėjimo diapazoną iki žemos kritinės arba minimalios reikšmės.", + "high-critical-state": "Aukšta kritinė būsena.", + "show-high-critical-scale": "Rodyti aukštos kritinės skalės reikšmę.", + "high-critical-scale": "Aukšta kritinė skalė.", + "high-critical-state-hint": "Slankiojo kablelio reikšmė, nurodanti aukštą kritinį diapazoną iki didžiausios skalės reikšmės.", + "low-critical-state": "Žemos kritinės būsenos reikšmė.", + "show-low-critical-scale": "Rodyti žemos kritinės būsenos reikšmę.", + "low-critical-scale": "Žema kritinė būsena.", + "low-critical-state-hint": "Slankiojo kablelio reikšmė, nurodanti žemos kritinės reikšmės diapazoną iki minimalios skalės reikšmės.", + "filter-color": "Filtro spalva", + "colors": "Spalvos", + "indicator-colors": "Indikatorių spalvos", + "enabled": "Įjungta", + "disabled": "Išjungta", + "on": "Įjungta", + "off": "Išjungta", + "on-off-state": "Įjungimo/Išjungimo būsena.", + "on-off-state-hint": "Nurodo, ar komponentas yra įjungtos, ar išjungtos būsenos.", + "on-update-state": "Įjungimo atnaujinimo būsena.", + "on-update-state-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atnaujintų būseną į „Įjungta“.", + "off-update-state": "Išjungimo atnaujinimo būsena.", + "off-update-state-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atnaujintų būseną į „Išjungta“.", + "voltage": "Įtampa", + "input-voltage": "Įėjimo įtampa.", + "input-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti įėjimo įtampos dydį.", + "output-voltage": "Išėjimo įtampa.", + "output-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti išėjimo įtampos dydį.", + "first-phase-voltage": "Pirmosios fazės įtampa.", + "second-phase-voltage": "Antrosios fazės įtampa.", + "third-phase-voltage": "Trečiosios fazės įtampa.", + "phase-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinės fazės įtampos dydį.", + "voltage-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinę įtampą.", + "current-voltage-color": "Dabartinės įtampos spalva.", + "phase-indicator-color": "Fazės indikatoriaus spalva.", + "measured": "Išmatuota.", + "measured-hint": "Slankiojo kablelio reikšmė, nurodanti energijos suvartojimą kilovatvalandėmis.", + "day-rate": "Dienos tarifas.", + "night-rate": "Naktinis tarifas.", + "off-peak-rate": "Ne piko tarifas.", + "peak-rate": "Piko tarifas.", + "export-rate": "Eksporto tarifas.", + "operating-mode": "Veikimo režimas.", + "bypass-mode": "Apeiti.", + "operating-mode-hint": "Sveikasis skaičius, nurodantis dabartinį veikimo režimą (0 – IŠJUNGTA, 1 – ĮJUNGTA, 2 – APEINIMAS).", + "connected": "Prijungta", + "connected-hint": "Nurodo, ar komponentas yra prijungtos būsenos.", + "disconnected": "Atjungta", + "indicator": "Indikatorius", + "operation-mode": "Veikimo režimas", + "operation-mode-hint": "Nurodo, ar keitiklis veikia tinklo (Mains), ar inverterio (Inverter) režimu.", + "operation-mode-indicators-color": "Veikimo režimo indikatorių spalva.", + "mains-on-mode": "Tinklas įjungtas.", + "inverter-on-mode": "Keitiklis įjungtas.", + "charging-mode": "Įkrovimo režimas", + "charging-mode-hint": "Sveikasis skaičius, nurodantis dabartinį įkrovimo režimą (1 – Greitas įkrovimas, 2 – Sugėrimas, 3 – Palaikomasis įkrovimas).", + "charging-mode-indicators-color": "Įkrovimo režimo indikatorių spalva.", + "inverter-faults": "Gedimai.", + "inverter-fault-indicators-color": "Gedimo indikatorių spalva.", + "overload-fault": "Perkrova", + "overload-fault-hint": "Nurodo, ar keitiklis yra perkrovos būsenoje.", + "low-battery-fault": "Silpna baterija", + "low-battery-fault-hint": "Nurodo, ar akumuliatorius yra per daug išsikrovęs.", + "temperature-fault": "Temperatūra", + "temperature-fault-hint": "Nurodo, ar keitiklyje yra aukšta temperatūra.", + "triangle": "Trikampis.", + "socket": "Lizdas.", + "left-button": "Kairysis mygtukas.", + "right-button": "Dešinysis mygtukas.", + "alarm-colors": "Aliarmo spalvos", + "hook-color": "Kabliuko spalva" + } + }, + "item": { + "selected": "Pasirinkta" + }, + "js-func": { + "no-return-error": "Funkcija privalo grąžinti reikšmę!", + "return-type-mismatch": "Funkcijos grąžinama reikšmė privalo būti '{{type}}' tipo!", + "tidy": "Sutvarkyti", + "mini": "Sumažinti", + "modules": "Moduliai", + "remove-module": "Pašalinti modulį", + "no-modules": "Modulių nesukonfigūruota", + "add-module": "Pridėti modulį", + "module-alias": "Pseudonimas", + "invalid-module-alias-name": "Neteisingas pseudonimo pavadinimas", + "module-resource": "JS modulio šaltinis", + "not-unique-module-aliases-error": "Modulių pseudonimai turi būti unikalūs!", + "show-module-info": "Rodyti modulio informaciją", + "show-module-source-code": "Rodyti modulio pirminį kodą", + "module-members": "Modulio nariai", + "module-no-members": "Modulis neturi eksportuotų narių", + "module-load-error": "Modulio įkėlimo klaida", + "source-code": "Pirminis kodas", + "source-code-load-error": "Pirminio kodo įkėlimo klaida", + "no-js-module-text": "JS modulių nerasta", + "no-js-module-matching": "Nerasta JS modulių, atitinkančių '{{module}}'." + }, + "key-val": { + "key": "Raktas", + "value": "Reikšmė", + "remove-entry": "Panaiknti įrašą", + "add-entry": "Pridėti įrašą", + "no-data": "Įrašų nėra" + }, + "layout": { + "layout": "Maketas", + "layouts": "Maketai", + "manage": "Valdyti maketus", + "settings": "Maketų nustatymai", + "color": "Spalva", + "main": "Pagrindinis", + "right": "Į dešinę", + "left": "Į kairę", + "select": "Pasirinkti maketą", + "percentage-width": "Procentinis plotis (%)", + "fixed-width": "Fiksuotas plotis (px)", + "left-width": "Kairysis stulpelis (%)", + "right-width": "Dešinysis stulpelis (%)", + "pick-fixed-side": "Fiksuota pusė: ", + "layout-fixed-width": "Fiksuotas plotis (px)", + "value-min-error": "Reikšmė turi būti dedesnė nei {{min}}{{unit}}", + "value-max-error": "Reikšmė turi būti mažesnė nei {{max}}{{unit}}", + "layout-fixed-width-required": "Fiksuotas plotis būtinas", + "right-width-percentage-required": "Dešinės pusės procentas būtinas", + "left-width-percentage-required": "kairės pusės procentas būtinas", + "divider": "Daliklis", + "right-side": "Dešinės pusės maketas", + "left-side": "Kairės pusės maketas", + "add-new-breakpoint": "Pridėti naują pertraukos tašką", + "breakpoint": "Pertraukos taškas", + "breakpoints": "Pertraukos taškai", + "copy-from": "Kopijuoti iš", + "size": "Dydis", + "delete-breakpoint-title": "Ar tikrai norite ištrinti pertraukos tašką '{{name}}'?", + "delete-breakpoint-text": "Atkreipkite dėmesį, kad po patvirtinimo pertraukos taškas bus negrįžtamai pašalintas, o nustatymai bus atkurti pagal numatytąjį pertraukos tašką." + }, + "legend": { + "direction": "Elementų išdėstymo kryptis", + "position": "Legendos pozicija", + "show-values": "Rodyti reikšmes", + "min-option": "Min", + "max-option": "Max", + "average-option": "Vidurkis", + "total-option": "Viso", + "latest-option": "Naujausi", + "sort-legend": "Legendoje rodyti duomenų raktus", + "show-max": "Rodyti didžiausią reišmę", + "show-min": "Rodyti mažiausią reikšmę", + "show-avg": "Rodyti vidutinę reikšmę", + "show-total": "Rodyti suminę rekšmę", + "show-latest": "Rodyti naujausią reikšmę", + "settings": "Legendos nustatymai", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "total": "Viso", + "latest": "Naujausia", + "Min": "Min", + "Max": "Max", + "Avg": "Vid.", + "Total": "Viso", + "Latest": "Vėliausia", + "comparison-time-ago": { + "previousInterval": "(ankstesnis intervalas)", + "customInterval": "(pasirinktinis intervalas)", + "days": "(prieš dieną)", + "weeks": "(prieš savaitę)", + "months": "(prieš mėnesį)", + "years": "(prieš metus)" + }, + "column-title": "Stulpelio pavadinimas", + "label": "Etiketė", + "value": "Reikšmė" + }, + "login": { + "login": "Prisijungti", + "request-password-reset": "Prašyti slaptažodžio atstatymo", + "reset-password": "Atstatyti slaptažodį", + "create-password": "Sukurti slaptažodį", + "two-factor-authentication": "Dviejų veiksnių autentifikavimas", + "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", + "password-again": "Pakartokite slaptažodį", + "sign-in": "Prisijungti", + "username": "Vartotojo vardas (el. paštas)", + "remember-me": "Prisiminti mane", + "forgot-password": "Pamiršote slaptažodį?", + "password-reset": "Slaptažodis atstatytas", + "expired-password-reset-message": "Jūsų slaptažodžio galiojimas baigėsi! Nustatykite naują slaptažodį.", + "new-password": "Naujas slaptažodis", + "new-password-again": "Pakartokite naują slaptažodį", + "password-link-sent-message": "Atstatymo nuoroda išsiųsta", + "email": "El. paštas", + "invalid-email-format": "Neteisingas el. pašto formatas.", + "login-with": "Prisijungti kaip {{name}}", + "or": "arba", + "error": "Prisijungimo klaida", + "verify-your-identity": "Patvirtinkite savo tapatybę", + "select-way-to-verify": "Pasirinkite tapatybės patvirtinimo būdą", + "resend-code": "Siųsti kodą iš naujo", + "resend-code-wait": "Galima siųsti iš naujo po { time, plural, =1 {1 sekundės} other {# sekundžių} }", + "try-another-way": "Bandykite kitu būdu", + "totp-auth-description": "Įveskite saugos kodą iš autentifikavimo programėlės.", + "totp-auth-placeholder": "Kodas", + "sms-auth-description": "Saugos kodas išsiųstas į jūsų telefoną adresu {{contact}}.", + "sms-auth-placeholder": "SMS kodas", + "email-auth-description": "Saugos kodas išsiųstas į jūsų el. paštą adresu {{contact}}.", + "email-auth-placeholder": "El. pašto kodas", + "backup-code-auth-description": "Įveskite vieną iš atsarginių kodų.", + "backup-code-auth-placeholder": "Atsarginis kodas", + "activation-link-expired": "Aktyvavimo nuorodos galiojimas baigėsi", + "activation-link-expired-message": "Profilio aktyvavimo nuoroda nebegalioja. Galite grįžti į prisijungimo puslapį ir gauti naują el. laišką.", + "reset-password-link-expired": "Slaptažodžio atstatymo nuoroda nebegalioja", + "reset-password-link-expired-message": "Slaptažodžio atstatymo nuoroda nebegalioja. Galite grįžti į prisijungimo puslapį ir gauti naują el. laišką." + }, + "mobile": { + "add-application": "Pridėti programėlę", + "app-id": "Programėlės svetainės asociacijos ID", + "app-id-required": "Programėlės svetainės asociacijos ID yra būtinas", + "app-id-pattern": "Neteisingas programėlės svetainės asociacijos ID formatas", + "app-store-link": "App Store nuoroda", + "app-store-link-required": "App Store nuoroda yra būtina", + "application-details": "Programėlės informacija", + "application-package": "Programėlės paketas", + "application-secret": "Programėlės slaptas raktas", + "application-secret-required": "Programėlės slaptas raktas yra būtinas", + "application": "Programėlė", + "applications": "Programėlės", + "copy-app-id": "Kopijuoti programėlės ID", + "copy-app-store-link": "Kopijuoti App Store nuorodą", + "copy-application-package": "Kopijuoti programėlės paketą", + "copy-application-secret": "Kopijuoti programėlės slaptą raktą", + "copy-google-play-link": "Kopijuoti Google Play nuorodą", + "copy-sha256-certificate-fingerprints": "Kopijuoti SHA256 sertifikato atspaudus", + "delete-application": "Ištrinti programėlę", + "delete-application-button-text": "Suprantu pasekmes, ištrinti programėlę", + "delete-application-text": "Šio veiksmo atšaukti nebus galima. Tai visam laikui ištrins jūsų programėlę.
    Jei nenorite jos ištrinti visam laikui, galite laikinai ją sustabdyti.
    Norėdami vis tiek ištrinti programėlę, įveskite patvirtinimo frazę \"{{phrase}}\".", + "delete-application-title-short": "Ar tikrai norite ištrinti programėlę '{{name}}'?", + "delete-application-text-short": "Būkite atsargūs – po patvirtinimo programėlė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-application-phrase": "ištrinti programėlę", + "delete-applications-bundle-text": "Būkite atsargūs – po patvirtinimo mobilusis paketas ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-applications-bundle-title": "Ar tikrai norite ištrinti mobilųjį paketą '{{bundleName}}'?", + "generate-application-secret": "Generuoti programėlės slaptą raktą", + "google-play-link": "Google Play nuoroda", + "google-play-link-required": "Google Play nuoroda yra būtina", + "latest-version": "Naujausia versija", + "min-version": "Minimali versija", + "invalid-version-pattern": "Neteisingas versijos formatas. Naudokite formatą: major.minor.patch (pvz., 1.0.0).", + "mobile-center": "Mobilusis centras", + "mobile-package": "Programėlės paketas", + "mobile-package-max-length": "Programėlės pavadinimas turi būti trumpesnis nei 256 simboliai", + "mobile-package-required": "Programėlės paketas yra būtinas", + "mobile-package-pattern": "Neteisingas programėlės paketo formatas", + "mobile-package-title": "Programėlės pavadinimas", + "mobile-package-title-max-length": "Programėlės pavadinimas turi būti trumpesnis nei 256 simboliai", + "no-application": "Programėlių nerasta", + "no-bundles": "Paketų nerasta", + "platform-type": "Platformos tipas", + "search-application": "Ieškoti programėlių", + "search-bundles": "Ieškoti paketų", + "set": "Nustatyti", + "sha256-certificate-fingerprints": "SHA256 sertifikato atspaudai", + "sha256-certificate-fingerprints-required": "SHA256 sertifikato atspaudai yra būtini", + "sha256-certificate-fingerprints-pattern": "Neteisingas SHA256 sertifikato atspaudo formatas", + "show-hidden-pages": "Rodyti paslėptus puslapius", + "status": "Būsena", + "status-type": { + "deprecated": "Nebenaudojama", + "draft": "Juodraštis", + "published": "Paskelbta", + "suspended": "Sustabdyta" + }, + "store-information": "Parduotuvės informacija", + "version-information": "Versijos informacija", + "min-version-release-notes": "Minimalios versijos išleidimo pastabos", + "latest-version-release-notes": "Naujausios versijos išleidimo pastabos", + "bundle": "Paketas", + "bundles": "Paketai", + "add-bundle": "Pridėti paketą", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas", + "title-cannot-contain-only-spaces": "Pavadinimas negali būti tik iš tarpų", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "oauth-clients": "OAuth 2.0 klientai", + "android-app": "Android programa", + "android-application": "Android aplikacija", + "ios-app": "iOS programa", + "ios-application": "iOS aplikacija", + "invalid-store-link": "Neteisinga parduotuvės nuoroda", + "enable-oauth": "Įjungti OAuth 2.0", + "enable-self-registration": "Įjungti savarankišką registraciją", + "edit-bundle": "Redaguoti paketą", + "description": "Aprašymas", + "basic-settings": "Pagrindiniai nustatymai", + "no-application-matching": "Nerasta programų, atitinkančių '{{entity}}'.", + "no-bundle-matching": "Nerasta paketų, atitinkančių '{{entity}}'.", + "application-required": "Programa būtina.", + "bundle-required": "Paketas būtinas.", + "no-application-text": "Programų nerasta", + "no-bundle-text": "Paketų nerasta", + "layout": "Išdėstymas", + "pages": "Puslapiai", + "hide-all-pages": "Slėpti visus puslapius", + "reset-to-default-pages": "Atstatyti numatytuosius puslapius", + "add-specific-page": "Pridėti konkretų puslapį", + "visible": "Matomas", + "hidden": "Paslėptas", + "reset-to-page-default": "Atstatyti puslapį į numatytąją būseną", + "mobile-599": "Mobilus (maks. 599px)", + "tablet-959": "Planšetė (maks. 959px)", + "max-element-number": "Maksimalus elementų skaičius", + "page-name": "Puslapio pavadinimas", + "page-name-required": "Puslapio pavadinimas būtinas.", + "page-name-cannot-contain-only-spaces": "Puslapio pavadinimas negali būti tik iš tarpų.", + "page-name-max-length": "Puslapio pavadinimas turi būti trumpesnis nei 256 simboliai", + "page-type": "Puslapio tipas", + "pages-types": { + "dashboard": "Valdymo skydas", + "web-view": "Žiniatinklio peržiūra", + "custom": "Pasirinktinis" + }, + "url": "URL", + "invalid-url-format": "Neteisingas URL formatas", + "path": "Kelias", + "invalid-path-format": "Neteisingas kelio formatas", + "custom-page": "Pasirinktinis puslapis", + "edit-page": "Redaguoti puslapį", + "edit-custom-page": "Redaguoti pasirinktą puslapį", + "delete-page": "Ištrinti puslapį", + "qr-code-widget": "QR kodo valdiklis", + "type-here": "Įveskite čia", + "configuration-dialog": "Konfigūracijos dialogas", + "configuration-app": "Konfigūracijos programa", + "configuration-step": { + "prepare-environment-title": "Paruošti kūrimo aplinką", + "prepare-environment-text": "Flutter ThingsBoard mobiliajai programai reikalingas Flutter SDK. Vadovaukitės instrukcijomis, kad nustatytumėte Flutter SDK.", + "get-source-code-title": "Gauti programos išeities kodą", + "get-source-code-text": "Galite gauti Flutter ThingsBoard mobiliosios programos išeities kodą, klonuodami jį iš GitHub saugyklos:", + "configure-app-settings-title": "Sukonfigūruoti programos nustatymus", + "configure-app-settings-text": "Atsisiųskite konfigūracijos failą ir įdėkite jį į projekto, kurį klonavote ankstesniame žingsnyje, šakninį katalogą.", + "download-file": "Atsisiųsti failą", + "run-app-title": "Paleisti programą", + "run-app-text": "Paleiskite programą kaip aprašyta jūsų IDE.\nJei naudojate terminalą, paleiskite programą naudodami šią komandą:", + "more-information": "Išsami informacija pateikta mūsų „Pradžios vadove“.", + "getting-started": "Pradžios vadovas" + } + }, + "notification": { + "action-button": "Veiksmo mygtukas", + "action-type": "Veiksmo tipas", + "active": "Aktyvus", + "add-notification-recipients-group": "Pridėti pranešimų gavėjų grupę", + "add-notification-template": "Pridėti pranešimo šabloną", + "add-recipient": "Pridėti gavėją", + "add-recipients": "Pridėti gavėjus", + "add-rule": "Pridėti taisyklę", + "add-stage": "Pridėti etapą", + "add-template": "Pridėti šabloną", + "after": "Po", + "alarm-assignment-trigger-settings": "Aliarmo priskyrimo trigerio nustatymai", + "alarm-comment-trigger-settings": "Aliarmo komentaro trigerio nustatymai", + "alarm-trigger-settings": "Aliarmo trigerio nustatymai", + "all": "Visi", + "api-feature-hint": "Jei laukas tuščias, trigeris bus taikomas visoms API funkcijoms", + "api-usage-trigger-settings": "API naudojimo trigerio nustatymai", + "new-platform-version-trigger-settings": "Naujos platformos versijos trigerio nustatymai", + "rate-limits-trigger-settings": "Viršytų užklausų ribų trigerio nustatymai", + "task-processing-failure-trigger-settings": "Užduočių apdorojimo klaidų trigerio nustatymai", + "resources-shortage-trigger-settings": "Išteklių trūkumo trigerio nustatymai", + "at-least-one-should-be-selected": "Reikia pasirinkti bent vieną", + "basic-settings": "Pagrindiniai nustatymai", + "button-text": "Mygtuko tekstas", + "button-text-required": "Mygtuko tekstas yra privalomas", + "button-text-max-length": "Mygtuko tekstas turi būti trumpesnis nei {{ length }} simbolių", + "compose": "Sukurti", + "conversation": "Pokalbis", + "conversation-required": "Pokalbis yra privalomas", + "copy-notification-template": "Kopijuoti pranešimo šabloną", + "copy-rule": "Kopijuoti taisyklę", + "copy-template": "Kopijuoti šabloną", + "create-new": "Sukurti naują", + "created": "Sukurta", + "customize-messages": "Tinkinti pranešimus", + "cpu-threshold": "CPU riba", + "delete-notification-text": "Būkite atsargūs – po patvirtinimo pranešimas taps neatstatomas.", + "delete-notification-title": "Ar tikrai norite ištrinti pranešimą?", + "delete-notifications-text": "Būkite atsargūs – po patvirtinimo pranešimai taps neatstatomi.", + "delete-notifications-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 pranešimą} other {# pranešimus} }?", + "delete-recipient-text": "Būkite atsargūs – po patvirtinimo gavėjas taps neatstatomas.", + "delete-recipient-title": "Ar tikrai norite ištrinti gavėją '{{recipientName}}'?", + "delete-recipients-text": "Būkite atsargūs – po patvirtinimo gavėjai taps neatstatomi.", + "delete-recipients-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 gavėją} other {# gavėjus} }?", + "delete-request-text": "Būkite atsargūs – po patvirtinimo užklausa taps neatstatoma.", + "delete-request-title": "Ar tikrai norite ištrinti užklausą?", + "delete-requests-text": "Būkite atsargūs – po patvirtinimo užklausos taps neatstatomos.", + "delete-requests-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 užklausą} other {# užklausas} }?", + "delete-rule-text": "Būkite atsargūs – po patvirtinimo taisyklė taps neatstatoma.", + "delete-rule-title": "Ar tikrai norite ištrinti taisyklę '{{ruleName}}'?", + "delete-rules-text": "Būkite atsargūs – po patvirtinimo taisyklės taps neatstatomos.", + "delete-rules-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 taisyklę} other {# taisykles} }?", + "delete-template-text": "Būkite atsargūs – po patvirtinimo šablonas taps neatstatomas.", + "delete-template-title": "Ar tikrai norite ištrinti šabloną '{{templateName}}'?", + "delete-templates-text": "Būkite atsargūs – po patvirtinimo šablonai taps neatstatomi.", + "delete-templates-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 šabloną} other {# šablonus} }?", + "deleted": "Ištrinta", + "delivery-method": { + "delivery-method": "Pristatymo būdas", + "email": "El. paštas", + "email-preview": "El. pašto pranešimo peržiūra", + "slack": "Slack", + "slack-preview": "Slack pranešimo peržiūra", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams pranešimo peržiūra", + "sms": "SMS", + "sms-preview": "SMS pranešimo peržiūra", + "web": "Žiniatinklis", + "web-preview": "Žiniatinklio pranešimo peržiūra", + "mobile-app": "Mobilioji programėlė", + "mobile-app-preview": "Mobiliosios programėlės pranešimo peržiūra" + }, + "delivery-method-not-configure-click": "Pristatymo būdas nesukonfigūruotas. Spustelėkite, kad nustatytumėte.", + "delivery-method-not-configure-contact": "Pristatymo būdas nesukonfigūruotas. Susisiekite su sistemos administratoriumi.", + "delivery-methods": "Pristatymo būdai", + "description": "Aprašymas", + "device-activity-trigger-settings": "Įrenginio aktyvumo trigerio nustatymai", + "device-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems įrenginiams", + "device-profiles-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems įrenginių profiliams", + "disabled": "Išjungta", + "edge-trigger-settings": "Edge trigerio nustatymai", + "edge-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visoms Edge instancijoms", + "edit-notification-recipients-group": "Redaguoti pranešimų gavėjų grupę", + "edit-notification-template": "Redaguoti pranešimo šabloną", + "edit-rule": "Redaguoti taisyklę", + "edit-template": "Redaguoti šabloną", + "enabled": "Įjungta", + "entities-limit-trigger-settings": "Objektų limito trigerio nustatymai", + "entity-action-trigger-settings": "Objekto veiksmo trigerio nustatymai", + "entity-type": "Objekto tipas", + "escalation-chain": "Eskalacijos grandinė", + "failed-send": "Siuntimo klaidos", + "fails": "{ count, plural, =1 {1 klaida} other {# klaidos} }", + "filter": "Filtras", + "first-recipient": "Pirmasis gavėjas", + "inactive": "Neaktyvus", + "inbox": "Gautieji", + "notification-inbox": "Pranešimai / Gautieji", + "input-field-support-templatization": "Įvesties laukas palaiko šablonizaciją.", + "input-fields-support-templatization": "Įvesties laukai palaiko šablonizaciją.", + "link": "Nuoroda", + "link-required": "Nuoroda yra privaloma", + "link-max-length": "Nuoroda turi būti ne ilgesnė kaip {{ length }} simbolių", + "link-type": { + "dashboard": "Atidaryti valdymo skydą", + "link": "Atidaryti URL nuorodą" + }, + "loading-notifications": "Įkeliami pranešimai...", + "management": "Pranešimų valdymas", + "mark-all-as-read": "Pažymėti visus kaip skaitytus", + "mark-as-read": "Pažymėti kaip skaitytą", + "message": "Žinutė", + "message-required": "Žinutė yra privaloma", + "message-max-length": "Žinutė turi būti ne ilgesnė kaip {{ length }} simbolių", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas", + "new-notification": "Naujas pranešimas", + "no-inbox-notification": "Pranešimų nerasta", + "no-notification-request": "Pranešimo užklausų nėra", + "no-notification-templates": "Pranešimų šablonų nerasta", + "no-notifications-yet": "Kol kas pranešimų nėra", + "no-recipients-notification": "Nėra pranešimo gavėjų", + "no-recipients-matching": "Gavėjų atitinkančių '{{entity}}' nerasta.", + "no-recipients-text": "Gavėjų nerasta", + "no-rule": "Nėra sukonfigūruotos taisyklės", + "no-rules-notification": "Nėra pranešimų taisyklių", + "no-severity-found": "Prioritetų nerasta", + "no-severity-matching": "Prioritetas '{{severity}}' nerastas.", + "no-template-matching": "Resurso, atitinkančio '{{template}}', nerasta.", + "create-new-template": "Sukurti naują!", + "not-found-slack-recipient": "Slack gavėjas nerastas", + "notification": "Pranešimas", + "notification-center": "Pranešimų centras", + "notification-tap-action": "Veiksmas paspaudus pranešimą", + "notification-tap-action-hint": "Jei neįjungta, bus naudojamas numatytasis aliarmo prietaisų skydas", + "notify": "Pranešti", + "notify-again": "Pranešti dar kartą", + "notify-alarm-action": { + "acknowledged": "Aliarmas patvirtintas", + "assigned": "Aliarmas priskirtas", + "cleared": "Aliarmas išvalytas", + "created": "Aliarmas sukurtas", + "severity-changed": "Aliarmo prioritetas pakeistas", + "unassigned": "Aliarmas nepriskirtas" + }, + "notify-on": "Pranešti apie", + "notify-on-comment-update": "Pranešti apie komentaro atnaujinimą", + "notify-on-required": "„Pranešti apie“ yra privalomas", + "notify-on-unassign": "Pranešti apie nepriskyrimą", + "notify-only-user-comments": "Pranešti tik apie vartotojų komentarus", + "only-rule-chain-lifecycle-failures": "Tik taisyklių grandinės ciklo klaidos", + "only-rule-node-lifecycle-failures": "Tik taisyklių mazgų ciklo klaidos", + "platform-users": "Platformos vartotojai", + "ram-threshold": "RAM riba", + "rate-limits": "Naudojimo apribojimai", + "rate-limits-hint": "Jei laukas tuščias, trigeris bus taikomas visiems naudojimo apribojimams", + "recipient": "Gavėjas", + "recipient-group": "Gavėjų grupė", + "recipient-type": { + "affected-tenant-administrators": "Paveikti nuomininko administratoriai", + "affected-user": "Paveiktas vartotojas", + "all-users": "Visi vartotojai", + "customer-users": "Klientų vartotojai", + "system-administrators": "Sistemos administratoriai", + "tenant-administrators": "Nuomininko administratoriai", + "user-filters": "Vartotojų filtras", + "user-list": "Vartotojų sąrašas", + "users-entity-owner": "Subjekto savininko vartotojai" + }, + "recipients": "Gavėjai", + "notification-recipient": "Pranešimo gavėjas", + "notification-recipient-required": "Pranešimo gavėjas yra privalomas.", + "notification-recipients": "Pranešimai / Gavėjai", + "recipients-count": "{ count, plural, =1 {1 gavėjas} other {# gavėjai} }", + "recipients-required": "Gavėjai yra privalomi", + "refresh-allow-delivery-method": "Atnaujinti leidžiamą pristatymo metodą", + "request-search": "Užklausos paieška", + "request-status": { + "processing": "Vykdoma", + "scheduled": "Suplanuota", + "sent": "Išsiųsta" + }, + "review": "Peržiūra", + "rule": "Taisyklė", + "rule-chain-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visoms taisyklių grandinėms", + "rule-engine-events-trigger-settings": "Taisyklių variklio įvykių trigerio nustatymai", + "rule-engine-filter": "Taisyklių variklio filtras", + "rule-name": "Taisyklės pavadinimas", + "rule-name-required": "Pavadinimas yra privalomas", + "rule-disable": "Išjungti pranešimų taisyklę", + "rule-enable": "Įjungti pranešimų taisyklę", + "rule-node-filter": "Taisyklių mazgo filtras", + "rules": "Taisyklės", + "notification-rules": "Pranešimai / Taisyklės", + "scheduler-later": "Suplanuoti vėlesniam laikui", + "search-notification": "Ieškoti pranešimų", + "search-recipients": "Ieškoti gavėjų", + "search-rules": "Ieškoti taisyklių", + "search-templates": "Ieškoti šablonų", + "see-documentation": "Peržiūrėti dokumentaciją", + "selected-notifications": "{ count, plural, =1 {1 pranešimas} other {# pranešimai} } pasirinkta", + "selected-recipients": "{ count, plural, =1 {1 gavėjas} other {# gavėjai} } pasirinkta", + "selected-requests": "{ count, plural, =1 {1 užklausa} other {# užklausos} } pasirinkta", + "selected-rules": "{ count, plural, =1 {1 taisyklė} other {# taisyklės} } pasirinkta", + "selected-template": "{ count, plural, =1 {1 šablonas} other {# šablonai} } pasirinkta", + "send-notification": "Siųsti pranešimą", + "sent": "Išsiųsta", + "setup": "Nustatymai", + "notification-sent": "Pranešimai / Išsiųsti", + "set-entity-from-notification": "Nustatyti objektą iš pranešimo į valdymo suvestinę", + "slack-chanel-type": "Slack kanalo tipas", + "slack-chanel-types": { + "direct": "Tiesioginė žinutė", + "private-channel": "Privatus kanalas", + "public-channel": "Viešas kanalas" + }, + "start-from-scratch": "Pradėti nuo pradžių", + "status": "Būsena", + "stop-escalation-alarm-status-become": "Sustabdyti eskalaciją, kai aliarmo būsena tampa:", + "storage-threshold": "Saugyklos riba", + "subject": "Tema", + "subject-required": "Tema yra privaloma", + "subject-max-length": "Temos ilgis turi būti ne didesnis nei {{ length }} simbolių", + "template": "Šablonas", + "template-name": "Šablono pavadinimas", + "template-required": "Šablonas yra privalomas", + "template-type": { + "alarm": "Aliarmas", + "alarm-assignment": "Aliarmo priskyrimas", + "alarm-comment": "Aliarmo komentaras", + "api-usage-limit": "API naudojimo limitas", + "device-activity": "Įrenginio aktyvumas", + "entities-limit": "Objektų limitas", + "entity-action": "Objekto veiksmas", + "general": "Bendras", + "rule-engine-lifecycle-event": "Taisyklių variklio gyvavimo ciklo įvykis", + "rule-node": "Taisyklių mazgas", + "new-platform-version": "Nauja platformos versija", + "rate-limits": "Viršytos normos ribos", + "edge-communication-failure": "Edge ryšio klaida", + "edge-connection": "Edge prisijungimas", + "task-processing-failure": "Užduoties apdorojimo klaida", + "resources-shortage": "Išteklių trūkumas" + }, + "templates": "Šablonai", + "notification-templates": "Pranešimai / Šablonai", + "tenant-profiles-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems nuomininkų profiliams", + "tenants-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems nuomininkams", + "threshold": "Riba", + "theme-color": "Temos spalva", + "time": "Laikas", + "track-rule-node-events": "Stebėti taisyklių mazgų įvykius", + "trigger": { + "alarm": "Aliarmas", + "alarm-assignment": "Aliarmo priskyrimas", + "alarm-comment": "Aliarmo komentaras", + "api-usage-limit": "API naudojimo limitas", + "device-activity": "Įrenginio aktyvumas", + "entities-limit": "Objektų limitas", + "entity-action": "Objekto veiksmas", + "rule-engine-lifecycle-event": "Taisyklių variklio gyvavimo ciklo įvykis", + "new-platform-version": "Nauja platformos versija", + "rate-limits": "Viršytos normos ribos", + "edge-connection": "Edge prisijungimas", + "edge-communication-failure": "Edge ryšio klaida", + "task-processing-failure": "Užduoties apdorojimo klaida", + "resources-shortage": "Išteklių trūkumas", + "trigger": "Trigeris", + "trigger-required": "Trigeris yra privalomas" + }, + "type": "Tipas", + "unread": "Neskaitytas", + "updated": "Atnaujintas", + "use-deprecated-webhook-connectors": "Naudoti pasenusius Webhook jungiklius", + "use-old-api": "Naudoti seną API", + "use-template": "Naudoti šabloną", + "view-all": "Peržiūrėti visus", + "warning": "Įspėjimas", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL yra privalomas", + "workflow-url": "Darbo eigos URL", + "workflow-url-required": "Darbo eigos URL yra privalomas", + "channel-name": "Kanalas", + "channel-name-required": "Kanalas yra privalomas", + "settings": { + "notification-settings": "Pranešimų nustatymai", + "reset-all": "Atstatyti visus nustatymus", + "reset-all-title": "Ar tikrai norite atstatyti formą?", + "reset-all-text": "Po patvirtinimo nustatymų forma bus atstatyta į numatytąsias reikšmes ir išsaugota.", + "type": "Tipas", + "enable-all": "Įjungti visus", + "disable-all": "Išjungti visus", + "delivery-not-configured": "Pristatymo metodas nesukonfigūruotas" + } + }, + "ota-update": { + "add": "Pridėti paketą", + "assign-firmware": "Priskirta programinė aparatinė įranga", + "assign-firmware-required": "Priskirta programinė aparatinė įranga yra privaloma", + "assign-software": "Priskirta programinė įranga", + "assign-software-required": "Priskirta programinė įranga yra privaloma", + "auto-generate-checksum": "Automatiškai generuoti kontrolinę sumą", + "checksum": "Kontrolinė suma", + "checksum-hint": "Jei kontrolinė suma tuščia, ji bus sugeneruota automatiškai", + "checksum-algorithm": "Kontrolinės sumos algoritmas", + "checksum-copied-message": "Paketo kontrolinė suma nukopijuota į iškarpinę", + "change-firmware": "Programinės aparatinės įrangos pakeitimas gali sukelti { count, plural, =1 {1 įrenginio} other {# įrenginių} } atnaujinimą.", + "change-software": "Programinės įrangos pakeitimas gali sukelti { count, plural, =1 {1 įrenginio} other {# įrenginių} } atnaujinimą.", + "change-ota-setting-title": "Ar tikrai norite pakeisti OTA nustatymus?", + "chose-compatible-device-profile": "Įkeltas paketas bus pasiekiamas tik įrenginiams su pasirinktu profiliu.", + "chose-firmware-distributed-device": "Pasirinkite programinę aparatinę įrangą, kuri bus paskirstyta įrenginiams", + "chose-software-distributed-device": "Pasirinkite programinę įrangą, kuri bus paskirstyta įrenginiams", + "content-type": "Turinio tipas", + "copy-checksum": "Kopijuoti kontrolinę sumą", + "copy-direct-url": "Kopijuoti tiesioginį URL", + "copyId": "Kopijuoti paketo ID", + "copied": "Nukopijuota!", + "delete": "Ištrinti paketą", + "delete-ota-update-text": "Būkite atsargūs – po patvirtinimo OTA atnaujinimas bus negrįžtamai pašalintas.", + "delete-ota-update-title": "Ar tikrai norite ištrinti OTA atnaujinimą '{{title}}'?", + "delete-ota-updates-text": "Būkite atsargūs – po patvirtinimo visi pasirinkti OTA atnaujinimai bus pašalinti.", + "delete-ota-updates-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 OTA atnaujinimą} other {# OTA atnaujinimus} }?", + "description": "Aprašymas", + "direct-url": "Tiesioginis URL", + "direct-url-copied-message": "Paketo tiesioginis URL nukopijuotas į iškarpinę", + "direct-url-required": "Tiesioginis URL yra privalomas", + "download": "Atsisiųsti paketą", + "drop-file": "Įmeskite paketo failą arba spustelėkite norėdami įkelti.", + "drop-package-file-or": "Nutempkite paketo failą arba", + "file-name": "Failo pavadinimas", + "file-size": "Failo dydis", + "file-size-bytes": "Failo dydis baitais", + "idCopiedMessage": "Paketo ID nukopijuotas į iškarpinę", + "no-firmware-matching": "Nerasta jokių suderinamų programinės aparatinės įrangos OTA atnaujinimo paketų, atitinkančių '{{entity}}'.", + "no-firmware-text": "Nėra suderinamų programinės aparatinės įrangos OTA atnaujinimo paketų.", + "no-packages-text": "Paketų nerasta", + "no-software-matching": "Nerasta jokių suderinamų programinės įrangos OTA atnaujinimo paketų, atitinkančių '{{entity}}'.", + "no-software-text": "Nėra suderinamų programinės įrangos OTA atnaujinimo paketų.", + "ota-update": "OTA atnaujinimas", + "ota-update-details": "OTA atnaujinimo informacija", + "ota-updates": "OTA atnaujinimai", + "package-file": "Paketo failas", + "package-type": "Paketo tipas", + "packages-repository": "Paketų saugykla", + "search": "Ieškoti paketų", + "selected-package": "{ count, plural, =1 {1 paketas} other {# paketai} } pasirinkta", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "types": { + "firmware": "Programinė aparatinė įranga", + "software": "Programinė įranga" + }, + "upload-binary-file": "Įkelti dvejetainį failą", + "use-external-url": "Naudoti išorinį URL", + "version": "Versija", + "version-required": "Versija yra privaloma.", + "version-tag": "Versijos žyma", + "version-tag-hint": "Pasirinktinė žyma turi atitikti paketo versiją, kurią praneša jūsų įrenginys.", + "version-max-length": "Versija turi būti trumpesnė nei 256 simboliai", + "warning-after-save-no-edit": "Įkėlus paketą, pavadinimo, versijos, įrenginio profilio ir paketo tipo keisti nebegalėsite." + }, + "position": { + "top": "Viršus", + "bottom": "Apačia", + "left": "Kairioji pusė", + "right": "Dešinioji pusė" + }, + "profile": { + "profile": "Profilis", + "last-login-time": "Paskutinio prisijungimo laikas", + "change-password": "Pakeisti slaptažodį", + "current-password": "Dabartinis slaptažodis", + "copy-jwt-token": "Kopijuoti JWT raktą", + "jwt-token": "JWT raktas", + "token-valid-till": "Raktas galioja iki", + "tokenCopiedSuccessMessage": "JWT raktas nukopijuotas į iškarpinę", + "tokenCopiedWarnMessage": "JWT raktas nebegalioja! Atnaujinkite puslapį." + }, + "profiles": { + "profiles": "Profiliai" + }, + "security": { + "security": "Saugumas", + "general-settings": "Bendrieji saugumo nustatymai", + "access-token": "Prieigos raktas", + "access-token-required": "Prieigos raktas yra privalomas", + "clientId": "Kliento ID", + "clientId-required": "Kliento ID yra privalomas", + "username": "Vartotojo vardas", + "username-required": "Vartotojo vardas yra privalomas", + "ca-cert": "CA sertifikatas", + "2fa": { + "2fa": "Dviejų veiksnių autentifikavimas", + "2fa-description": "Dviejų veiksnių autentifikavimas apsaugo jūsų paskyrą nuo neteisėtos prieigos. Prisijungdami turėsite įvesti saugos kodą.", + "authenticate-with": "Galite autentifikuotis naudodami:", + "disable-2fa-provider-text": "Išjungus {{name}}, jūsų paskyra taps mažiau saugi.", + "disable-2fa-provider-title": "Ar tikrai norite išjungti {{name}}?", + "get-new-code": "Gauti naują kodą", + "main-2fa-method": "Naudoti kaip pagrindinį dviejų veiksnių autentifikavimo būdą", + "dialog": { + "activation-step-description-email": "Kitą kartą prisijungiant bus prašoma įvesti saugos kodą, kuris bus išsiųstas jūsų el. pašto adresu.", + "activation-step-description-sms": "Kitą kartą prisijungiant bus prašoma įvesti saugos kodą, kuris bus išsiųstas jūsų telefono numeriu.", + "activation-step-description-totp": "Kitą kartą prisijungiant turėsite pateikti dviejų veiksnių autentifikavimo kodą.", + "activation-step-label": "Aktyvacija", + "backup-code-description": "Atsispausdinkite atsarginius kodus, kad turėtumėte juos po ranka, kai reikės prisijungti prie paskyros. Kiekvienas atsarginis kodas gali būti naudojamas tik vieną kartą.", + "backup-code-warn": "Palikus šį puslapį, šie kodai nebebus rodomi. Išsaugokite juos saugiai.", + "download-txt": "Atsisiųsti (txt)", + "email-step-description": "Įveskite el. pašto adresą, kuris bus naudojamas kaip autentifikatorius.", + "email-step-label": "El. paštas", + "enable-email-title": "Įjungti el. pašto autentifikatorių", + "enable-sms-title": "Įjungti SMS autentifikatorių", + "enable-totp-title": "Įjungti autentifikavimo programėlę", + "enter-verification-code": "Įveskite 6 skaitmenų kodą", + "get-backup-code-title": "Gauti atsarginį kodą", + "next": "Toliau", + "scan-qr-code": "Nuskaitykite šį QR kodą naudodami autentifikavimo programėlę", + "send-code": "Siųsti kodą", + "sms-step-description": "Įveskite telefono numerį, kuris bus naudojamas kaip autentifikatorius.", + "sms-step-label": "Telefono numeris", + "success": "Sėkmingai!", + "totp-step-description-install": "Galite įdiegti tokias programėles kaip Google Authenticator, Authy arba Duo.", + "totp-step-description-open": "Atidarykite autentifikavimo programėlę savo telefone.", + "totp-step-label": "Gauti programėlę", + "verification-code": "6 skaitmenų kodas", + "verification-code-invalid": "Neteisingas kodo formatas", + "verification-code-incorrect": "Neteisingas patvirtinimo kodas", + "verification-code-many-request": "Per daug bandymų patikrinti kodą", + "verification-step-description": "Įveskite 6 skaitmenų kodą, kurį ką tik išsiuntėme adresu {{address}}", + "verification-step-label": "Patvirtinimas" + }, + "provider": { "email": "El. paštas", - "no-address": "Adresas nenurodytas", - "state-max-length": "Rajono pavadinimas negali viršyti 256 simbolių", - "phone-max-length": "Telefono numeris negali viršyti 256 simbolių", - "city-max-length": "Miesto pavadinimas negali viršyti 256 simbolių" - }, - "common": { - "username": "Vartoto vardas", - "password": "Slaptažodis", - "enter-username": "Įveskite vartotojo vardą", - "enter-password": "Įveskite slaptažodį", - "enter-search": "Įveskite paieškos kriterijus", - "created-time": "Sukūrimo laikas", - "loading": "Įkeliama...", - "proceed": "Tęsti", - "open-details-page": "Informacija", - "not-found": "Nerasta", - "documentation": "Dokumentacija" - }, - "converter": { - "converter": "Data converter", - "converters": "Data converters", - "select-converter": "Select data converter", - "no-converters-matching": "No data converters matching '{{entity}}' were found.", - "no-converters-found": "No data converters found.", - "converter-required": "Data converter is required", - "delete": "Delete converter", - "management": "Data converters management", - "add-converter-text": "Add new data converter", - "no-converters-text": "No data converters found", - "selected-converters": "{ count, plural, =1 {1 data converter} other {# data converters} } selected", - "delete-converter-title": "Are you sure you want to delete the data converter '{{converterName}}'?", - "delete-converter-text": "Be careful, after the confirmation the data converter and all related data will become unrecoverable.", - "delete-converters-title": "Are you sure you want to delete { count, plural, =1 {1 data converter} other {# data converters} }?", - "delete-converters-action-title": "Delete { count, plural, =1 {1 data converter} other {# data converters} }", - "delete-converters-text": "Be careful, after the confirmation all selected data converters will be removed and all related data will become unrecoverable.", - "events": "Events", - "add": "Add Data Converter", - "search": "Search data converters", - "converter-details": "Data converter details", - "details": "Details", - "copyId": "Copy converter Id", - "idCopiedMessage": "Converter Id has been copied to clipboard", - "debug-mode": "Debug mode", - "created-time": "Created time", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "decoder": "Decoder", - "encoder": "Encoder", - "test-decoder-fuction": "Test decoder function", - "test-encoder-fuction": "Test encoder function", - "test-with-this-message": "{{test}} with this message", - "decoder-input-params": "Decoder input parameters", - "encoder-input-params": "Encoder input parameters", - "payload": "Payload", - "payload-content-type": "Payload content type", - "payload-content": "Payload content", - "message": "Message", - "message-type": "Message type", - "message-type-required": "Message type is required", - "test": "Test", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "integration-metadata": "Integration metadata", - "integration-metadata-required": "Integration metadata entries can't be empty.", - "output": "Output", - "import": "Import converter", - "export": "Export converter", - "export-failed-error": "Unable to export converter: {{error}}", - "create-new-converter": "Create new converter", - "converter-file": "Converter file", - "invalid-converter-file-error": "Unable to import converter: Invalid converter data structure.", - "type": "Type", - "type-required": "Type is required.", - "type-uplink": "Uplink", - "type-downlink": "Downlink", - "update-only-keys-list": "Update only keys list", - "update-only-keys-list-hint": "The values associated with the provided keys will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", - "add-key": "Add key" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" + "email-description": "Naudokite saugos kodą, išsiųstą į jūsų el. pašto adresą, kad autentifikuotumėtės.", + "email-hint": "Autentifikavimo kodai siunčiami el. paštu adresu {{ info }}", + "sms": "SMS", + "sms-description": "Naudokite savo telefoną autentifikacijai. Prisijungiant išsiųsime jums saugos kodą SMS žinute.", + "sms-hint": "Autentifikavimo kodai siunčiami SMS žinute numeriu {{ info }}", + "totp": "Autentifikavimo programėlė", + "totp-description": "Naudokite programėles, tokias kaip Google Authenticator, Authy ar Duo, savo telefone autentifikacijai. Jos generuos prisijungimo kodą.", + "totp-hint": "Autentifikavimo programėlė sukonfigūruota jūsų paskyrai", + "backup_code": "Atsarginis kodas", + "backup-code-description": "Šie vienkartiniai spausdinami kodai leidžia prisijungti, kai neturite prieigos prie telefono, pavyzdžiui, keliaujant.", + "backup-code-hint": "Šiuo metu aktyvūs {{ info }} vienkartinio naudojimo kodai" + } }, - "color": { - "color": "Color", - "color-picker": "Color picker", - "primary-colors": "Primary colors", - "accent-colors": "Accent colors", - "no-color-selected": "No color selected" - }, - "customer": { - "all": "Visi", - "all-customers": "Visi klientai", - "groups": "Grupės", - "shared": "Bendrinami", - "hierarchy": "Hierarchija", - "customer": "Klientas", - "customers": "Klientai", - "management": "Klientų valdymas", - "dashboard": "Klientų skydelis", - "dashboards": "Kliientų skydeliai", - "devices": "Kliento įrenginiai", - "entity-views": "Kliento subjektų rodiniai", - "assets": "Kliento turtas", - "public-dashboards": "Vieši skydeliai", - "public-devices": "Vieši įrenginiai", - "public-assets": "Viešas turtas", - "public-entity-views": "Vieši subjektų rodiniai", - "add": "Pridėti klientą", - "delete": "Pašalinti klientą", - "manage-customer-user-groups": "Kliento vartotojų grupės", - "manage-customer-groups": "Klientų grupės", - "manage-customer-device-groups": "Kliento įrenginių grupės", - "manage-customer-asset-groups": "Kliento turto grupės", - "manage-customer-entity-view-groups": "Kliento subjektų rodinių grupės", - "manage-customer-edge-groups": "Manage customer edge groups", - "manage-customer-dashboard-groups": "Kliento skydelių grupės", - "manage-customer-users": "Kliento vartotojai", - "manage-customers": "Klientai", - "manage-customer-devices": "Kliento įrenginiai", - "manage-customer-entity-views": "Kliento subjektų rodiniai", - "manage-customer-dashboards": "Kliento skydeliai", - "manage-public-devices": "Vieši įrenginiai", - "manage-public-dashboards": "Vieši skydeliai", - "manage-customer-assets": "Kliento turtas", - "manage-customer-edges": "Manage customer edges", - "manage-public-assets": "Viešas turtas", - "add-customer-text": "Pridėti naują klientą", - "no-customers-text": "Klientų nėra", - "customer-details": "Informacija apie klientą", - "delete-customer-title": "Ar tikrai norite pašalinti klientą '{{customerTitle}}'?", - "delete-customer-text": "Būkite dėmesingi, po patvirtinimo, kliento ir su juo susijusios informacijos atkurti nebegalėsite.", - "delete-customers-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 klientą} other {# klientus} }?", - "delete-customers-action-title": "Pašalinti { count, plural, =1 {1 klientą} other {# klientus} }", - "delete-customers-text": "Būkite dėmesingi, po patvirtinimo, klientų ir su jais susijusios informacijos atkurti nebegalėsite.", - "manage-user-groups": "Vartotjų grupės", - "manage-asset-groups": "turto grupės", - "manage-device-groups": "Įrenginių grupės", - "manage-dashboard-groups": "Skydelių grupės", - "manage-entity-view-groups": "Subjektų rodinių grupės", - "manage-edge-groups": "Manage edge groups", - "manage-users": "Vartotojai", - "manage-assets": "Turtas", - "manage-devices": "Įrenginiai", - "manage-dashboards": "Skydeliai", - "manage-entity-views": "Subjektų rodiniai", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "details": "Informacija", - "events": "Įvykiai", - "copyId": "Kopijuoti kliento Id", - "idCopiedMessage": "Kliento Id nukopijuotas į iškarpinę", - "select-customer": "Pasirinkti klientą", - "no-customers-matching": "Klientų, atitinkančių '{{entity}}' nėra.", - "customer-required": "Klientas būtinas", - "select-group-to-add": "Pasirinkite grupę, į kurią pridėti pažymėtus klientus", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus klientus", - "remove-customers-from-group": "Ar tikrai { count, plural, =1 {1 klientą} other {# klientus} } norite pašalinti iš grupės '{{entityGroup}}'?", - "group": "Klientų grupė", - "list-of-groups": "{ count, plural, =1 {Viena kliento grupė} other {Sąrašas # klientų grupių} }", - "group-name-starts-with": "Klientų grupės, kurios prasideda '{{prefix}}'", - "select-default-customer": "Pasirinkite numatytąjį klientą", - "default-customer": "Numatytasis klientas", - "default-customer-required": "Numatytasis klientas būtinas, norint suderinti prietaisų skydelį valdytojo lygiu", - "allow-white-labeling": "Leisti tinkinti aplikaciją", - "search": "Klientų paieška", - "selected-customers": "Pasirinkta { count, plural, =1 {1 klientas} other {# kientai} }", - "edges": "Customer edge instances", - "manage-edges": "Manage edges" - }, - "customers-hierarchy": { - "customers-hierarchy": "Klientų hierarchija", - "open-nav-tree": "Atverti navigacijos medį", - "return-to-top-level": "Grįžti į aukščiausią lygį" - }, - "custom-menu": { - "custom-menu": "Vartotojo meniu", - "custom-menu-hint": "Aprašykite pasirinktinį meniu JSON formoje. Šiame JSON nurodomas vartotojo meniu elementų sąrašas." - }, - "custom-translation": { - "custom-translation": "Vartotojo vertimai", - "translation-map": "Vertimų žemėlapis", - "key": "Vertimo raktas", - "import": "Importuoti vertimą", - "export": "Eksportuoti vertimą", - "export-data": "Eksportuoti vertimo duomenis", - "import-data": "Importuoti vertimo duomenis", - "translation-file": "Vertimų failas", - "invalid-translation-file-error": "Nepavyksta importuoti vertimo failo: Neteisinga duomenų struktūra.", - "custom-translation-hint": "Aprašykite vartotojo vertimą JSON formatu žemiau. Šis JSON perrašys standartinį vertimą. Paspaudę 'Parsisiųsti lokalės failą', gausite standartinio vertimo failą, kuriame galite surasti reikalingas raktų-reikšmių poros ir jas pakeisti savo JSON.", - "download-locale-file": "Parsisųsti lokalės failą" - }, - "date": { - "last-update-n-ago": "Last update N ago", - "last-update-n-ago-text": "Last update {{ agoText }}", - "custom-date": "Custom date", - "format": "Format", - "preview": "Preview" - }, - "datetime": { - "date-from": "Data nuo", - "time-from": "Laikas nuo", - "date-to": "Data iki", - "time-to": "Laikas iki" - }, - "dashboard": { - "all": "Visi", - "all-dashboards": "Visi skydeliai", - "groups": "Grupės", - "shared": "Bendrinami", - "dashboard": "Skydelis", - "dashboards": "Skydeliai", - "management": "Skydelių valdymas", - "view-dashboards": "Paržiūrėti skydelius", - "add": "Pridėti skydelį", - "assign-dashboard-to-customer": "Skydelį (-ius) priskirti klientui", - "assign-dashboard-to-customer-text": "Pasirinkite skydelius, kuriuos norite priskirti klientui", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti skydelį (-ius)", - "assign-to-customer": "Priskirti kientui", - "unassign-from-customer": "Atsieti nuo kliento", - "make-public": "Skydelį padaryti viešą", - "make-private": "Skydelį padaryti privatų", - "manage-assigned-customers": "Valdyti priskirtus klientus", - "assigned-customers": "Priskirti klientai", - "assign-to-customers": "Skydelį (-ius) priskirti klientams", - "assign-to-customers-text": "Pasirinkite klientus, kuriems priskirti skydelį (-ius)", - "unassign-from-customers": "Skydelį (-ius) atsieti nuo klientų", - "unassign-from-customers-text": "Pasirinkite klientus, kuriems atsieti skydelį (-ius)", - "no-dashboards-text": "Skydelių nėra", - "no-widgets": "Valdikliai nesukonfigūruoti", - "add-widget": "Pridėti naują valdiklį", - "add-widget-button-text": "Pridėt valdiklį", - "title": "Pavadinimas", - "image": "Skydelio paveiksliukas", - "mobile-app-settings": "Mobiliosios aplikacijos nustatymai", - "mobile-order": "Skydelio eilės tvarka mobiliojoje aplikacijoje", - "mobile-hide": "Mobiliojoje aplikacijoje skydelį paslėpti", - "update-image": "Atnaujinti skydelio paveiksliuką", - "take-screenshot": "Padaryti ekrano nuotrauką", - "select-widget-title": "Pasirinkite valdiklį", - "select-widget-value": "{{title}}: pasirinkite valdiklį", - "select-widget-subtitle": "Galimų valdiklių sąrašas", - "delete": "Pašalinti skydelį", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "details": "Informacija", - "dashboard-details": "Informacija", - "add-dashboard-text": "Pridėti naują skydelį", - "assign-dashboards": "Priskirti skydelius", - "assign-new-dashboard": "Priskirti naują skydelį", - "assign-dashboards-text": "Priskirti { count, plural, =1 {1 skydelį} other {# skydelius} } klientams", - "unassign-dashboards-action-text": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo klientų", - "delete-dashboards": "Panaikinti skydelius", - "unassign-dashboards": "Atsieti skydelius", - "unassign-dashboards-action-title": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo kliento", - "delete-dashboard-title": "Ar tikrai norite pašalinti skydelį '{{dashboardTitle}}'?", - "delete-dashboard-text": "Būkite dėmesingi, po patvirtinimo, skydelio ir su juo susijusios informacijos atkurti nebegalėsite.", - "delete-dashboards-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }?", - "delete-dashboards-action-title": "Pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }", - "delete-dashboards-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti skydeliai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", - "unassign-dashboard-title": "Ar tikrai norite atsieti skydelį '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Po patvrtinimo skydelis bus atsietas ir klientas jo nebematys.", - "unassign-dashboard": "Atsieti skydelį", - "unassign-dashboards-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", - "unassign-dashboards-text": "Po patvirtinimo visi skydeliai bus atsieti ir klientas jų nebematys.", - "public-dashboard-title": "Skydelis dabar yra viešas", - "public-dashboard-text": "Skydelis {{dashboardTitle}} dabar yra viešas ir pasiekiamas per šią nuorodą:", - "public-dashboard-notice": "Pastaba: Įrenginiai turi būti vieši, jei norite matyti jų duomenis.", - "public-dashboard-link": "Vieša skydelio nuoroda", - "public-dashboard-link-text": "Viešą sydelį {{dashboardTitle}} galima atidaryti per šią nuorodą:", - "public-dashboard-link-notice": "Pastaba: Įrenginiai, turtas ir subjektai turi būti vieši, jei norite matyti jų duomenis.", - "make-private-dashboard-title": "Ar tikrai norite skydelį '{{dashboardTitle}}' padaryti privačiu?", - "make-private-dashboard-text": "Po patvirtinimo, skydelis taps privačiu ir jo nematys kiti vartotojai.", - "make-private-dashboard": "Skydelį padaryti privačiu", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Pasirinkite skydelį", - "no-dashboards-matching": "Skydelio, atitinkančio '{{entity}}' nėra.", - "dashboard-required": "Skydelis būtinas.", - "select-existing": "Pasirinkite egzistuojantį skydelį", - "create-new": "Sukurti naują skydelį", - "new-dashboard-title": "Naujas skydelio pavadinimas", - "open-dashboard": "Atidaryti skydelį", - "set-background": "Nustatyti foną", - "background-color": "Fono spalva", - "background-image": "Fono paveikslėlis", - "background-size-mode": "Fono dydis", - "no-image": "Paveikslėlis nepasirinktas", - "empty-image": "Nėra paveikslėlio", - "drop-image": "Užvilkite paveikslėlį arba spustelėkite ir pasirinkite failą.", - "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", - "cannot-upload-file": "Failo įkelti nepavyko", - "settings": "Nustatymai", - "columns-count": "Stulpelių skaičius", - "columns-count-required": "Stulpelių skaičius būtinas.", - "min-columns-count-message": "Minimalus stulpelių skaičius - 10.", - "max-columns-count-message": "Maksimalus stulpelių skaičius - 1000.", - "widgets-margins": "Paraštės tarp valdiklių dysis", - "margin-required": "Paraštės dydžio reikšmė būtina.", - "min-margin-message": "Minimali paraštės dydžio reikšmė - 0.", - "max-margin-message": "Maksimali paraštės dydžio reikšmė - 50.", - "horizontal-margin": "Horizontalios paraštės dydis", - "horizontal-margin-required": "Horizontalios paraštės dydžio reikšmė būtina.", - "min-horizontal-margin-message": "Minimali horizontalios paraštės dydžio reikšmė - 0", - "max-horizontal-margin-message": "Maksimali horizontalios paraštės dydžio reikšmė - 50.", - "vertical-margin": "Vertikalios paraštės dydis", - "vertical-margin-required": "Vertikalios paraštės dydžio reikšmė būtina.", - "min-vertical-margin-message": "Minimali vertikalios paraštės dydžio reikšmė - 0.", - "max-vertical-margin-message": "Maksimali vertikalios paraštės dydžio reikšmė - 50.", - "apply-outer-margin": "Taikyti paraštes maketo šonuose", - "autofill-height": "Automatiškai pritaikyti aukštį", - "mobile-layout": "Mobiliojo režimo išdėstymo nustatymai", - "mobile-row-height": "Mobiliojo režimo eilutės aukštis pikseliais", - "mobile-row-height-required": "Mobiliojo režimo eilutės aukštis būtinas.", - "min-mobile-row-height-message": "Minimalus mobiliojo režimo eilutės aukštis yra 5 pikseliai.", - "max-mobile-row-height-message": "Maksimalus mobiliojo režimo eilutės aukštis yra 200 pikselių.", - "title-settings": "Pavadinimo nustatymai", - "display-title": "Rodyt skydelio pavadinimą", - "title-color": "Pavadinimo spalva", - "toolbar-settings": "Įrankių juostos nustatymai", - "hide-toolbar": "Paslėpti įrankių juostą", - "toolbar-always-open": "Įrankių juostą laikyti atdarytą", - "display-dashboards-selection": "Rodyti skydelių pasirinkimą", - "display-entities-selection": "Rodyti subjektų pasirinkimą", - "display-filters": "Rodyti filtrus", - "display-dashboard-timewindow": "Rodyti laiko langą", - "display-dashboard-export": "Rodyti eksportą", - "display-update-dashboard-image": "Display update dashboard image", - "dashboard-logo-settings": "Dashboard logo settings", - "display-dashboard-logo": "Display logo in dashboard fullscreen mode", - "dashboard-logo-image": "Dashboard logo image", - "advanced-settings": "Advanced settings", - "dashboard-css": "Dashboard CSS", - "import": "Importuoti skydelį", - "export": "Eksportuoti skydelį", - "export-failed-error": "Skydelio importuoti nepavyko: {{error}}", - "export-pdf": "Eksportuoti kaip PDF", - "export-png": "Eksportuoti kaip PNG", - "export-jpg": "Eksportuoti kaip JPEG", - "export-json-config": "Eksportuoti JSON konfigūraciją", - "download-dashboard-progress": "Generuojamas skydelis {{reportType}} ...", - "create-new-dashboard": "Sukurti naują skydelį", - "dashboard-file": "Skydelio failas", - "invalid-dashboard-file-error": "Skydelio importuoti nepavyko: neteisinga duomenų struktūra.", - "dashboard-import-missing-aliases-title": "Konfigūruoti importuoto skydelio naudojamus pseudonimus", - "create-new-widget": "Sukurti naują valdiklį", - "import-widget": "importuoti valdiklį", - "widget-file": "valdiklio failas", - "invalid-widget-file-error": "Valdiklio importuoti nepavyko: neteisinga duomenų struktūra.", - "widget-import-missing-aliases-title": "Konfigūruoti importuoto valdiklio naudojamus pseudonimus", - "open-toolbar": "Atidaryti skydelio įrankių juostą", - "close-toolbar": "Uždaryti įrankių juostą", - "configuration-error": "Konfigūracijos klaida", - "alias-resolution-error-title": "Skydelio pseudonimų konfigūracijos klaida", - "invalid-aliases-config": "Įrenginių, atitinkančių pseudonimų filtrus, nėra.
    Susisiekite su sistemos administratoriumi.", - "select-devices": "Pasirinkite įrenginius", - "assignedToCustomer": "Priskirta klientui", - "assignedToCustomers": "Priskirta klientams", - "public": "Viešas", - "copyId": "Kopijuoti skydelio Id", - "idCopiedMessage": "Skydelio Id nukopijuotas į iškarpinę", - "public-link": "Vieša nuoroda", - "copy-public-link": "Kopijuoti viešą nuorodą", - "public-link-copied-message": "Skydelio vieša nuoroda nukopijuota į iškarpinę", - "manage-states": "Valdyti skydelio būsenas", - "states": "Skydelio būsenos", - "states-short": "Būsenos", - "search-states": "Skydelio būsenų paieška", - "selected-states": "Pasirinkta skydelio { count, plural, =1 {1 būsena} other {# būsenos} }", - "edit-state": "Redaguoti skydelio būseną", - "delete-state": "Panaikinti skydelio būseną", - "add-state": "Pridėti skydelio būseną", - "no-states-text": "Būsenų nėra", - "state": "Skydelio būsena", - "state-name": "Pavadinimas", - "state-name-required": "Skydelio pavadinimas būtinas.", - "state-id": "Būsenos Id", - "state-id-required": "Būsenos Id būtinas.", - "state-id-exists": "Skydelio būsena su tokiu pačiu Id jau yra.", - "is-root-state": "Pagrindinė būsena", - "delete-state-title": "Panaikinti skydelio būseną", - "delete-state-text": "Ar tikrai norite panaikinti skydelio būseną '{{stateName}}'?", - "show-details": "Rodyti detales", - "hide-details": "Slėpti detales", - "select-state": "Pasirinkite būseną", - "state-controller": "Būsenų valdiklis", - "search": "Skydelių paieška", - "selected-dashboards": "Pasirinkta { count, plural, =1 {1 skydelis} other {# skydeliai} }", - "home-dashboard": "Pagrindinis skydelis", - "home-dashboard-hide-toolbar": "Slėpti pagrindinio skydelio įrankių juostą", - "select-group-to-add": "Pasirinkite grupę, prie kurios pridėti pažymėtus skydelius", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pasirinktus skydelius", - "remove-dashboards-from-group": "Ar tikrai pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} } iš grupės '{{entityGroup}}'?", - "group": "Skydelių grupė", - "list-of-groups": "{ count, plural, =1 {Viena skydelių grupė} other {Sąrašas # skydelių grupių} }", - "group-name-starts-with": "Skydelių grupės, kurių pavadinimai prasideda '{{prefix}}'", - "unassign-dashboard-from-edge-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the edge.", - "unassign-dashboards-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-from-edge-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the edge.", - "assign-dashboard-to-edge": "Assign Dashboard(s) To Edge", - "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge", - "non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found", - "edit-mode": "Edit mode" - }, - "datakey": { - "settings": "Nustatymai", - "general": "Bendra", - "advanced": "Patyrusiam vartotojui", - "key": "Raktas", - "label": "Etiketė", - "color": "Spalva", - "units": "Specialieji simboliai, rodomi šalia reikšmės", - "decimals": "Skaičių kiekis po kablelio", - "data-generation-func": "Duomenų generavimo funkcija", - "use-data-post-processing-func": "Papildomo duomenų apdorojimo funkcija", - "configuration": "Duomenų raktų konfigūracija", - "timeseries": "Telemetrija", - "attributes": "Atributai", - "entity-field": "Subjekto laukas", - "alarm": "Įspėjimo laukai", - "timeseries-required": "Subjekto telemetrija būtina.", - "timeseries-or-attributes-required": "Subjekto telemetrija/atributai būtini.", - "alarm-fields-timeseries-or-attributes-required": "Įspėjimo laukai ar subjekto telemetrijos/atributai būtini.", - "maximum-timeseries-or-attributes": "Leidžiama daugiausiai { count, plural, =1 {1 viena telemetrija/atributas.} other {# telemetrijos/atributai} }", - "alarm-fields-required": "Įspėjimo laukai būtini.", - "function-types": "Funkcijų tipai", - "function-type": "Funkcijos tipas", - "function-types-required": "Funkcijos tipai būtini.", - "data-keys": "Duomenų raktai", - "data-key": "Duomenų raktas", - "data-keys-required": "Duomenų raktai būtini.", - "data-key-required": "Duomenų raktas būtinas.", - "alarm-keys": "Įspėjimo duomenų raktai", - "alarm-key": "Įspėjimo duomenų raktas", - "alarm-key-functions": "Alarm key functions", - "alarm-key-function": "Alarm key function", - "latest-keys": "Latest data keys", - "latest-key": "Latest data key", - "latest-key-functions": "Latest key functions", - "latest-key-function": "Latest key function", - "timeseries-keys": "Timeseries data keys", - "timeseries-key": "Timeseries data key", - "timeseries-key-functions": "Timeseries key functions", - "timeseries-key-function": "Timeseries key function", - "maximum-function-types": "Daugiausiai leidžiama { count, plural, =1 {1 funkcijos tipas.} other {# funkcijos tipai} }", - "time-description": "Dabartinės vertės laiko žyma;", - "value-description": "Dabartinė vertė;", - "prev-value-description": "Ankstesnio funkcijos kvietimo rezultatas;", - "time-prev-description": "Ankstesnės vertės laiko žyma;", - "prev-orig-value-description": "Pradinė ankstesnė vertė;", - "aggregation": "Agregavimas", - "aggregation-type-hint-common": "For performance reasons, the aggregated values calculation is available only for fixed time intervals like \"current day\", \"current month\", etc, and is not available for sliding window intervals like 'last 30 minutes' or 'last 24 hours'.", - "aggregation-type-none-hint": "Take latest value.", - "aggregation-type-min-hint": "Find minimum value among data points within a selected time window.", - "aggregation-type-max-hint": "Find maximum value among data points within a selected time window.", - "aggregation-type-avg-hint": "Calculate an average value among data points within a selected time window.", - "aggregation-type-sum-hint": "Sum all values of the data points within a selected time window.", - "aggregation-type-count-hint": "Total number of the data points within a selected time window.", - "delta-calculation": "Delta calculation", - "enable-delta-calculation": "Enable delta calculation", - "enable-delta-calculation-hint": "When enabled, the data key value is calculated based on the aggregated values for a selected time window and a specified comparison period. For performance reasons, the delta calculation is available only for history time windows and not for real-time values. For example, you may calculate the delta between the energy consumption for yesterday compared to the energy consumption for the day before yesterday.", - "delta-calculation-result": "Delta calculation result", - "delta-calculation-result-previous-value": "Previous value", - "delta-calculation-result-delta-absolute": "Delta (absolute)", - "delta-calculation-result-delta-percent": "Delta (percent)", - "source": "Source", - "latest": "Latest", - "latest-value": "Latest value", - "delta": "delta", - "percent": "percent", - "absolute": "absolute" - }, - "datasource": { - "type": "Duomenų šaltinio tipas", - "name": "Pavadinimas", - "label": "Etiketė", - "add-datasource-prompt": "Pridėkite duomenų šaltinį" + "password-requirement": { + "at-least": "Bent:", + "character": "{ count, plural, =1 {1 simbolis} other {# simboliai(-ų)} }", + "digit": "{ count, plural, =1 {1 skaitmuo} other {# skaitmenys(-ų)} }", + "incorrect-password-try-again": "Neteisingas slaptažodis. Bandykite dar kartą", + "lowercase-letter": "{ count, plural, =1 {1 mažoji raidė} other {# mažosios raidės(-ių)} }", + "new-passwords-not-match": "Nauji slaptažodžiai nesutampa", + "password-should-not-contain-spaces": "Slaptažodyje negali būti tarpų", + "password-not-meet-requirements": "Slaptažodis neatitinka reikalavimų", + "password-requirements": "Slaptažodžio reikalavimai", + "password-should-difference": "Naujas slaptažodis turi skirtis nuo dabartinio", + "special-character": "{ count, plural, =1 {1 specialus simbolis} other {# specialūs simboliai(-ių)} }", + "uppercase-letter": "{ count, plural, =1 {1 didžioji raidė} other {# didžiosios raidės(-ių)} }", + "at-most": "Daugiausia:" + } + }, + "relation": { + "relations": "Ryšiai", + "direction": "Kryptis", + "clear-relation-type": "Aiškus ryšio tipas", + "search-direction": { + "FROM": "Iš", + "TO": "Į" }, - "details": { - "details": "Informacija", - "edit-mode": "Redagavimo režimas", - "edit-json": "Redaguoti JSON", - "toggle-edit-mode": "Redagavimo režimas" + "direction-type": { + "FROM": "iš", + "TO": "į" }, - "device": { - "all": "Visi", - "all-devices": "Visi įrenginiai", - "groups": "Grupės", - "shared": "Bendrinami", - "device": "Įrenginys", - "device-required": "Įrenginys būtinas.", - "devices": "Įrenginiai", - "management": "Įrenginių valdymas", - "view-devices": "Peržiūrėti įrenginius", - "device-alias": "Įrenginio pseudonimas", - "device-type-max-length": "Įrenginio tipas negali viršyti 256 simbolių", - "aliases": "Įrenginių pseudonimai aliases", - "no-alias-matching": "'{{alias}}' nerasta.", - "no-aliases-found": "Pseudonimų nėra.", - "no-key-matching": "'{{key}}' nerasta.", - "no-keys-found": "Raktų nėra.", - "create-new-alias": "Sukurti naują!", - "create-new-key": "Sukurti naują!", - "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", - "configure-alias": "Sukonfigūruoti '{{alias}}' pseudonimą", - "no-devices-matching": "Įrenginių, attinkančių '{{entity}}' nėra.", - "alias": "Pseudonimas", - "alias-required": "Įrenginio pseudonimas būtinas.", - "remove-alias": "Panaikinti įrenginio pseudonimą", - "add-alias": "Pridėti įrenginio pseudonimą", - "name-starts-with": "Įrenginio pavadinimas prasideda", - "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%įrenginio_pavadinimo_fragmentas%', '%įrenginio_pavadinimo_pabaiga', 'įrenginio_pavadinimo_pradžia%'.", - "device-list": "Įrenginių sąrašas", - "use-device-name-filter": "Naudoti filtrą", - "device-list-empty": "Įrenginiai nepasirinkti.", - "device-name-filter-required": "Įrenginio pavadinimo filtras turi būti nustatytas.", - "device-name-filter-no-device-matched": "Įrenginių, kurių pavadinimas prasideda '{{device}}' nėra.", - "add": "Pridėti įrenginį", - "assign-to-customer": "Priskirti kientui", - "assign-device-to-customer": "Įrenginį (-ius) priskirti klientui", - "assign-device-to-customer-text": "Pasirinkite įrenginius, kuriuos norite priskirti klientui", - "make-public": "Įrenginį padaryti viešu", - "make-private": "Įrenginį padaryti privačiu", - "no-devices-text": "Įrenginių nėra", - "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti įrenginį (-ius)", - "device-details": "Įrenginio informacija", - "add-device-text": "Pridėti naują įrenginį", - "credentials": "Įgaliojimai", - "manage-credentials": "Valdyti įgaliojimus", - "delete": "Panaikinti įrenginį", - "assign-devices": "priskirti įrenginius", - "assign-devices-text": "Priskirti { count, plural, =1 {1 įrenginį} other {# įrenginius} } klientui", - "delete-devices": "Panaikinti įrenginius", - "unassign-from-customer": "Atseti nuo kliento", - "unassign-devices": "Atsieti įrenginius", - "unassign-devices-action-title": "Atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo kliento", - "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", - "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", - "unassign-devices-from-edge": "Unassign devices from edge", - "assign-new-device": "Priskirti naują įrenginį", - "make-public-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti viešu?", - "make-public-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys bus vieši ir prieinami kitiems vartotojams.", - "make-private-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti privačiu?", - "make-private-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys taps privatūs ir nebus prieinami kitiems vartotojams.", - "view-credentials": "Peržiūrėti įgaliojimus", - "delete-device-title": "Ar tikrai norite panaikinti įrenginį '{{deviceName}}'?", - "delete-device-text": "Būkite dėmesingi, po patvirtinimo įrenginys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite", - "delete-devices-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "delete-devices-action-title": "Panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }", - "delete-devices-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti įrenginiai ir su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "unassign-device-title": "Ar tikrai norite atsieti įrenginį '{{deviceName}}'?", - "unassign-device-text": "Po patvirtinimo įrenginys bus atsietas ir klientas jo nebematys.", - "unassign-device": "Atsieti įrenginį", - "unassign-devices-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "unassign-devices-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti nuo kliento.", - "device-credentials": "įrenginio įgaliojimai", - "loading-device-credentials": "Įkeliami įrenginio įgaliojimai...", - "credentials-type": "Įgaliojimų tipas", - "access-token": "Prieigos raktas", - "access-token-required": "Prieigos raktas būtinas.", - "access-token-invalid": "Prieigos rakto ilgis turi būti nuo 1 iki 32 simbolių.", - "certificate-pem-format": "Certificate in PEM format", - "certificate-pem-format-required": "Certificate is required.", - "copy-access-token": "Copy Access token", - "copy-certificate": "Copy Certificate", - "copy-client-id": "Copy Client ID", - "copy-user-name": "Copy User name", - "copy-password": "Copy Password", - "generate-client-id": "Generate Client ID", - "generate-user-name": "Generate User name", - "generate-password": "Generate Password", - "generate-access-token": "Generate Access Token", - "lwm2m-security-config": { - "identity": "Client Identity", - "identity-required": "Client Identity is required.", - "identity-tooltip": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", - "client-key": "Client Key", - "client-key-required": "Client Key is required.", - "client-key-tooltip-prk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", - "client-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", - "endpoint": "Endpoint Client Name", - "endpoint-required": "Endpoint Client Name is required.", - "client-public-key": "Client public key", - "client-public-key-hint": "If client public key is empty, the trusted certificate will be used", - "client-public-key-tooltip": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format!", - "mode": "Security config mode", - "client-tab": "Client Security Config", - "client-certificate": "Client certificate", - "bootstrap-tab": "Bootstrap Client", - "bootstrap-server": "Bootstrap Server", - "lwm2m-server": "LwM2M Server", - "client-publicKey-or-id": "Client Public Key or Id", - "client-publicKey-or-id-required": "Client Public Key or Id is required.", - "client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", - "client-publicKey-or-id-tooltip-rpk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", - "client-publicKey-or-id-tooltip-x509": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format", - "client-secret-key": "Client Secret Key", - "client-secret-key-required": "Client Secret Key is required.", - "client-secret-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", - "client-secret-key-tooltip-prk": "RPK secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!", - "client-secret-key-tooltip-x509": "X509 secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!" - }, - "client-id": "Client ID", - "client-id-pattern": "Contains invalid character.", - "user-name": "User Name", - "user-name-required": "User Name is required.", - "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", - "password": "Password", - "secret": "Secret", - "secret-required": "Secret is required.", - "device-type": "Įrenginio tipas", - "device-type-required": "Įrenginio tipas būtinas.", - "select-device-type": "Pasirinkite įrenginio tipą", - "enter-device-type": "Įveskite įrenginio tipą", - "any-device": "Bet kuris įrenginys", - "no-device-types-matching": "Įrenginio tipų, atitinkančių '{{entitySubtype}}', nėra.", - "device-type-list-empty": "Nepasirinktas įrenginio tipas!", - "device-types": "Įrenginių tipai", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "label-max-length": "Etiketė negali viršyti 256 simbolių", - "description": "Aprašymas", + "from-relations": "Išeinantys ryšiai", + "to-relations": "įeinantys ryšiai", + "selected-relations": "Pasirinkta { count, plural, =1 {1 ryšys} other {# ryšiai} } selected", + "type": "Tipas", + "to-entity-type": "Į subjekto tipą", + "to-entity-name": "Į subjektą", + "from-entity-type": "iš subjektų tipo", + "from-entity-name": "Iš subjekto", + "to-entity": "į subjektą", + "from-entity": "Iš subjekto", + "delete": "panaikinti ryšį", + "relation-type": "Ryšio tipas", + "relation-type-required": "Ryšio tipas būtinas.", + "relation-type-max-length": "Ryšio tipas negali viršyti 256 simbolių", + "any-relation-type": "Bet kuris tipas", + "add": "pridėti ryšį", + "edit": "Redaguoti ryšį", + "delete-to-relation-title": "Ar tikrai norite panaikinti ryšį su '{{entityName}}' subjektu?", + "delete-to-relation-text": "Būkite dėmesingi, po patvirtinimo '{{entityName}}' subjektas neteks ryšio su dabartiniu subjektu.", + "delete-to-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-to-relations-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti ryšiai bus panaikinti ir atitinkami subjektai neteks ryšių su dabartiniu subjektu.", + "delete-from-relation-title": "Ar tikrai norite panaikinti ryšį iš '{{entityName}}' subjekto?", + "delete-from-relation-text": "Būkite dėmesingi, po patvirtinimo, dabartinis subjektas neteks ryšio su '{{entityName}}' subjektu.", + "delete-from-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-from-relations-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti ryšiai bus panaikinti ir dabartinis subjektas neteks ryšių su atitinkamais subjektais.", + "remove-relation-filter": "Panaikinti ryšių filtrą", + "remove-filter": "Panakinti filtrą", + "add-relation-filter": "Pridėti ryšių filtrą", + "any-relation": "Bet kuris ryšys", + "relation-filters": "Ryšių filtrai", + "relation-filter": "Relation filter", + "additional-info": "Papildoma informacija (JSON)", + "invalid-additional-info": "Papildomos JSON informacijos išanalizuoti nepavyko.", + "no-relations-text": "Ryšių nėra", + "not": "Ne" + }, + "resource": { + "add": "Pridėti išteklių", + "all-types": "Visi", + "copyId": "Kopijuoti ištekliaus ID", + "delete": "Ištrinti išteklių", + "delete-resource-text": "Būkite atsargūs, po patvirtinimo šis išteklius bus negrįžtamai pašalintas.", + "delete-resource-title": "Ar tikrai norite ištrinti išteklių '{{resourceTitle}}'?", + "delete-resources-action-title": "Ištrinti { count, plural, =1 {1 išteklių} other {# išteklius(-ių)} }", + "delete-resources-text": "Atkreipkite dėmesį, kad pasirinkti ištekliai bus ištrinti, net jei jie naudojami įrenginių profiliuose.", + "delete-resources-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 išteklių} other {# išteklius(-ių)} }?", + "download": "Atsisiųsti išteklių", + "drop-file": "Įkelkite išteklių failą arba spustelėkite norėdami pasirinkti failą.", + "drop-resource-file-or": "Vilkdami įkelkite išteklių failą arba", + "empty": "Išteklius tuščias", + "file-name": "Failo pavadinimas", + "idCopiedMessage": "Išteklius ID nukopijuotas į iškarpinę", + "no-resource-matching": "Nerasta ištekliaus, atitinkančio '{{widgetsBundle}}'.", + "no-resource-text": "Išteklių nerasta", + "open-widgets-bundle": "Atidaryti valdiklių paketą", + "resource": "Išteklius", + "resource-file": "Išteklių failas", + "resource-files": "Išteklių failai", + "resource-library-details": "Išteklių informacija", + "resource-type": "Išteklių tipas", + "resources-library": "Išteklių biblioteka", + "search": "Ieškoti išteklių", + "selected-resources": "{ count, plural, =1 {1 išteklius} other {# ištekliai(-ių)} } pasirinkta", + "system": "Sistema", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "type": { + "jks": "JKS", + "js-module": "JS modulis", + "lwm2m-model": "LWM2M modelis", + "pkcs-12": "PKCS #12" + }, + "resource-sub-type": "Potipio", + "sub-type": { + "image": "Paveikslėlis", + "scada-symbol": "SCADA simbolis", + "extension": "Plėtinys", + "module": "Modulis" + } + }, + "javascript": { + "add": "Pridėti JavaScript išteklių", + "delete": "Ištrinti JavaScript išteklių", + "delete-javascript-resource-text": "Būkite atsargūs – po patvirtinimo šis JavaScript išteklius bus nebeatkuriamas.", + "delete-javascript-resource-title": "Ar tikrai norite ištrinti JavaScript išteklių '{{resourceTitle}}'?", + "delete-javascript-resources-action-title": "Ištrinti JavaScript { count, plural, =1 {1 išteklių} other {# išteklius} }", + "delete-javascript-resources-text": "Atkreipkite dėmesį, kad pasirinkti JavaScript ištekliai, net jei jie naudojami JavaScript funkcijose, bus ištrinti.", + "delete-javascript-resources-title": "Ar tikrai norite ištrinti JavaScript { count, plural, =1 {1 išteklių} other {# išteklius} }?", + "delete-javascript-resource-in-use-text": "Jei vis tiek norite ištrinti šį JavaScript išteklių, spustelėkite mygtuką Ištrinti vis tiek.", + "download": "Atsisiųsti JavaScript išteklių", + "upload-from-file": "Įkelti JavaScript iš failo", + "resource-file": "JavaScript ištekliaus failas", + "drop-file": "Įmeskite JavaScript failą arba spustelėkite, kad pasirinktumėte failą įkėlimui.", + "drop-resource-file-or": "Vilkite ir numeskite JavaScript failą arba", + "javascript-library": "JavaScript biblioteka", + "javascript-type": "JavaScript tipas", + "javascript-resource-details": "JavaScript ištekliaus detalės", + "javascript-resource-is-in-use": "JavaScript išteklius naudojamas kituose objektuose", + "javascript-resources-are-in-use": "JavaScript ištekliai naudojami kituose objektuose", + "javascript-resource-is-in-use-text": "JavaScript išteklius '{{title}}' nebuvo ištrintas, nes jį naudoja šie objektai:", + "javascript-resources-are-in-use-text": "Ne visi JavaScript ištekliai buvo ištrinti, nes jie naudojami kituose objektuose.
    Galite peržiūrėti susijusius objektus spustelėdami mygtuką Nuorodos atitinkamoje eilutėje.
    Jei vis tiek norite ištrinti šiuos JavaScript išteklius, pasirinkite juos lentelėje ir spustelėkite mygtuką Ištrinti pasirinktus.", + "search": "Ieškoti JavaScript išteklių", + "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript išteklius} other {# JavaScript ištekliai} } pasirinkta", + "no-javascript-resource-text": "JavaScript išteklių nerasta", + "all-types": "Visi", + "module-script": "Modulio scenarijus" + }, + "rpc": { + "error": { + "target-device-is-not-set": "Tikslinis įrenginys nenustatytas!", + "invalid-target-entity": "RPC komandos nepalaikomos {{entityType}} objektui.", + "failed-to-resolve-target-device": "Nepavyko nustatyti tikslo įrenginio!", + "request-timeout": "Užklausos laikas baigėsi", + "rpc-http-error": "Klaida: {{status}} - {{statusText}}" + } + }, + "rulechain": { + "rulechain": "Taisyklių grandinė", + "rulechain-events": "Taisyklių grandinės įvykiai", + "rulechains": "Taisyklių grandinės", + "root": "Pagrindinė", + "delete": "Ištrinti taisyklių grandinę", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "description": "Aprašymas", + "add": "Pridėti taisyklių grandinę", + "set-root": "Padaryti pagrindine", + "set-root-rulechain-title": "Ar tikrai norite taisyklių grandinę '{{ruleChainName}}' padaryti pagrindine?", + "set-root-rulechain-text": "Patvirtinus ši taisyklių grandinė taps pagrindine ir tvarkys visus įeinančius transporto pranešimus.", + "delete-rulechain-title": "Ar tikrai norite ištrinti taisyklių grandinę '{{ruleChainName}}'?", + "delete-rulechain-text": "Atsargiai, po patvirtinimo ši taisyklių grandinė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-rulechains-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }?", + "delete-rulechains-action-title": "Ištrinti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }", + "delete-rulechains-text": "Atsargiai, po patvirtinimo visos pažymėtos taisyklių grandinės bus pašalintos ir visi susiję duomenys bus prarasti.", + "add-rulechain-text": "Pridėti naują taisyklių grandinę", + "no-rulechains-text": "Taisyklių grandinių nerasta", + "rulechain-details": "Taisyklių grandinės detalės", + "details": "Detalės", + "events": "Įvykiai", + "system": "Sistema", + "import": "Importuoti taisyklių grandinę", + "export": "Eksportuoti taisyklių grandinę", + "export-failed-error": "Nepavyko eksportuoti taisyklių grandinės: {{error}}", + "create-new-rulechain": "Sukurti naują taisyklių grandinę", + "rulechain-file": "Taisyklių grandinės failas", + "invalid-rulechain-file-error": "Nepavyko importuoti taisyklių grandinės: netinkama duomenų struktūra.", + "copyId": "Kopijuoti taisyklių grandinės ID", + "idCopiedMessage": "Taisyklių grandinės ID nukopijuotas į iškarpinę", + "select-rulechain": "Pasirinkti taisyklių grandinę", + "no-rulechains-matching": "Taisyklių grandinių, atitinkančių '{{entity}}', nerasta.", + "rulechain-required": "Taisyklių grandinė yra privaloma", + "management": "Taisyklių valdymas", + "debug-mode": "Derinimo režimas", + "search": "Ieškoti taisyklių grandinių", + "selected-rulechains": "{ count, plural, =1 {1 taisyklių grandinė} other {# taisyklių grandinės} } pasirinkta", + "open-rulechain": "Atidaryti taisyklių grandinę", + "edge-template-root": "Edge šablono pagrindas", + "assign-to-edge": "Priskirti Edge", + "edge-rulechain": "Edge taisyklių grandinė", + "unassign-rulechain-from-edge-text": "Patvirtinus, taisyklių grandinė bus atšaukta ir nebebus pasiekiama Edge įrenginiui.", + "unassign-rulechains-from-edge-title": "Ar tikrai norite atšaukti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }?", + "unassign-rulechains-from-edge-text": "Patvirtinus visos pažymėtos taisyklių grandinės bus atšauktos ir nebebus prieinamos Edge įrenginiui.", + "assign-rulechain-to-edge-title": "Priskirti taisyklių grandinę (-es) Edge įrenginiui", + "assign-rulechain-to-edge-text": "Pasirinkite taisyklių grandines, kurias norite priskirti Edge įrenginiui", + "set-edge-template-root-rulechain": "Padaryti taisyklių grandinę Edge šablono pagrindine", + "set-edge-template-root-rulechain-title": "Ar tikrai norite taisyklių grandinę '{{ruleChainName}}' padaryti Edge šablono pagrindine?", + "set-edge-template-root-rulechain-text": "Patvirtinus ši taisyklių grandinė taps pagrindine naujai kuriamiems Edge įrenginiams.", + "invalid-rulechain-type-error": "Nepavyko importuoti taisyklių grandinės: netinkamas tipas. Tikimasi tipo {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Automatiškai priskirti taisyklių grandinę Edge įrenginiams kūrimo metu", + "set-auto-assign-to-edge-title": "Ar tikrai norite automatiškai priskirti Edge taisyklių grandinę '{{ruleChainName}}' naujai kuriamiems Edge įrenginiams?", + "set-auto-assign-to-edge-text": "Patvirtinus ši taisyklių grandinė bus automatiškai priskiriama Edge įrenginiams jų kūrimo metu.", + "unset-auto-assign-to-edge": "Nepriskirti taisyklių grandinės Edge įrenginiams kūrimo metu", + "unset-auto-assign-to-edge-title": "Ar tikrai nenorite automatiškai priskirti Edge taisyklių grandinės '{{ruleChainName}}' naujai kuriamiems Edge įrenginiams?", + "unset-auto-assign-to-edge-text": "Patvirtinus ši taisyklių grandinė nebus automatiškai priskiriama Edge įrenginiams.", + "unassign-rulechain-title": "Ar tikrai norite atšaukti taisyklių grandinę '{{ruleChainName}}'?", + "unassign-rulechains": "Atšaukti taisyklių grandines" + }, + "rulenode": { + "rule-node-events": "Taisyklių mazgų įvykiai", + "details": "Detalės", + "events": "Įvykiai", + "search": "Ieškoti mazgų", + "open-node-library": "Atidaryti mazgų biblioteką", + "close-node-library": "Uždaryti mazgų biblioteką", + "add": "Pridėti taisyklės mazgą", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "type": "Tipas", + "rule-node-description": "Taisyklių mazgo aprašymas", + "delete": "Ištrinti taisyklės mazgą", + "select-all-objects": "Pažymėti visus mazgus ir jungtis", + "deselect-all-objects": "Atžymėti visus mazgus ir jungtis", + "delete-selected-objects": "Ištrinti pažymėtus mazgus ir jungtis", + "delete-selected": "Ištrinti pažymėtus", + "create-nested-rulechain": "Sukurti įdėtą taisyklių grandinę", + "select-all": "Pažymėti visus", + "copy-selected": "Kopijuoti pažymėtus", + "deselect-all": "Atžymėti visus", + "rulenode-details": "Taisyklių mazgo detalės", + "debug-mode": "Derinimo režimas", + "singleton": "Vienetinis (Singleton)", + "configuration": "Konfigūracija", + "link": "Jungtis", + "link-details": "Taisyklių mazgo jungties detalės", + "add-link": "Pridėti jungtį", + "link-label": "Jungties pavadinimas", + "link-label-required": "Jungties pavadinimas yra privalomas.", + "custom-link-label": "Tinkintas jungties pavadinimas", + "custom-link-label-required": "Tinkintas jungties pavadinimas yra privalomas.", + "link-labels": "Jungčių pavadinimai", + "link-labels-required": "Jungčių pavadinimai yra privalomi.", + "no-link-labels-found": "Jungčių pavadinimų nerasta", + "no-link-label-matching": "Jungtis '{{label}}' nerasta.", + "create-new-link-label": "Sukurti naują!", + "type-filter": "Filtras", + "type-filter-details": "Filtruoti įeinančias žinutes pagal konfigūruotas sąlygas", + "type-enrichment": "Praturtinimas", + "type-enrichment-details": "Papildyti žinutės metaduomenis papildoma informacija", + "type-transformation": "Transformacija", + "type-transformation-details": "Keisti žinutės turinį arba metaduomenis", + "type-action": "Veiksmas", + "type-action-details": "Atlikti specialų veiksmą", + "type-external": "Išorinis", + "type-external-details": "Sąveikauja su išorine sistema", + "type-rule-chain": "Taisyklių grandinė", + "type-rule-chain-details": "Perduoda įeinančias žinutes nurodytai taisyklių grandinei", + "type-flow": "Srautas", + "type-flow-details": "Organizuoja žinučių srautą", + "type-input": "Įvestis", + "type-input-details": "Taisyklių grandinės loginė įvestis, perduodanti žinutes tolesniam susijusiam mazgui", + "type-unknown": "Nežinomas", + "type-unknown-details": "Nesprendžiamas taisyklių mazgas", + "directive-is-not-loaded": "Nurodyta konfigūracijos direktyva '{{directiveName}}' nėra prieinama.", + "ui-resources-load-error": "Nepavyko įkelti konfigūracijos UI išteklių.", + "invalid-target-rulechain": "Nepavyko nustatyti paskirties taisyklių grandinės!", + "test-script-function": "Testuoti skripto funkciją", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", + "message": "Žinutė", + "message-type": "Žinutės tipas", + "select-message-type": "Pasirinkti žinutės tipą", + "message-type-required": "Žinutės tipas yra privalomas", + "metadata": "Metaduomenys", + "metadata-required": "Metaduomenų įrašai negali būti tušti.", + "output": "Išvestis", + "test": "Testuoti", + "help": "Pagalba", + "reset-debug-settings": "Atstatyti derinimo nustatymus visuose mazguose", + "test-with-this-message": "{{test}} su šia žinute", + "queue-hint": "Pasirinkite eilę, į kurią bus persiunčiamos žinutės. Numatytoji eilė – 'Main'.", + "queue-singleton-hint": "Pasirinkite eilę žinučių perdavimui kelių egzempliorių aplinkoje. Numatytoji eilė – 'Pagrindinis'." + }, + "rule-node-config": { + "id": "Id", + "additional-info": "Papildoma informacija", + "advanced-settings": "Išplėstiniai nustatymai", + "create-entity-if-not-exists": "Sukurti naują objektą, jei jis neegzistuoja", + "create-entity-if-not-exists-hint": "Jei įjungta, naujas objektas su nurodytais parametrais bus sukurtas, nebent toks jau egzistuoja. Esami objektai bus naudojami tokie, kokie yra, ryšio užmezgimui.", + "select-device-connectivity-event": "Pasirinkite įrenginio ryšio įvykį", + "entity-name-pattern": "Pavadinimo šablonas", + "device-name-pattern": "Įrenginio pavadinimas", + "asset-name-pattern": "Turto pavadinimas", + "entity-view-name-pattern": "Objekto peržiūros pavadinimas", + "customer-title-pattern": "Kliento pavadinimas", + "dashboard-name-pattern": "Valdymo skydelio pavadinimas", + "user-name-pattern": "Naudotojo el. paštas", + "edge-name-pattern": "Edge pavadinimas", + "entity-name-pattern-required": "Pavadinimo šablonas yra privalomas", + "entity-name-pattern-hint": "Pavadinimo šablono laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "copy-message-type": "Kopijuoti pranešimo tipą", + "entity-type-pattern": "Tipo šablonas", + "entity-type-pattern-required": "Tipo šablonas yra privalomas", + "message-type-value": "Pranešimo tipo reikšmė", + "message-type-value-required": "Pranešimo tipo reikšmė yra privaloma", + "message-type-value-max-length": "Pranešimo tipo reikšmė turi būti trumpesnė nei 256 simboliai", + "output-message-type": "Išvesties pranešimo tipas", + "entity-cache-expiration": "Objektų talpyklos galiojimo laikas (sek.)", + "entity-cache-expiration-hint": "Nurodo maksimalų laiką, kiek galima saugoti rastų objektų įrašus. Reikšmė 0 reiškia, kad įrašai niekada nesibaigs.", + "entity-cache-expiration-required": "Objektų talpyklos galiojimo laikas yra privalomas.", + "entity-cache-expiration-range": "Objektų talpyklos galiojimo laikas turi būti didesnis arba lygus 0.", + "customer-name-pattern": "Kliento pavadinimas", + "customer-name-pattern-required": "Kliento pavadinimas yra privalomas", + "customer-name-pattern-hint": "Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "create-customer-if-not-exists": "Sukurti naują klientą, jei jis neegzistuoja", + "unassign-from-customer": "Atšaukti priskyrimą klientui, jei siuntėjas yra valdymo skydelis", + "unassign-from-customer-tooltip": "Tik valdymo skydeliai gali būti priskirti keliems klientams vienu metu.\nJei pranešimo siuntėjas yra valdymo skydelis, turite aiškiai nurodyti kliento pavadinimą, kad jį atšauktumėte.", + "customer-cache-expiration": "Klientų talpyklos galiojimo laikas (sek.)", + "customer-cache-expiration-hint": "Nurodo maksimalų laiką, kiek galima saugoti rastų klientų įrašus. Reikšmė 0 reiškia, kad įrašai niekada nesibaigs.", + "customer-cache-expiration-required": "Klientų talpyklos galiojimo laikas yra privalomas.", + "customer-cache-expiration-range": "Klientų talpyklos galiojimo laikas turi būti didesnis arba lygus 0.", + "interval-start": "Intervalo pradžia", + "interval-end": "Intervalo pabaiga", + "time-unit": "Laiko vienetas", + "fetch-mode": "Gavimo režimas", + "order-by-timestamp": "Rikiuoti pagal laiko žymą", + "limit": "Riba", + "limit-hint": "Mažiausia riba – 2, didžiausia – 1000. Jei norite gauti vieną įrašą, pasirinkite gavimo režimą 'Pirmas' arba 'Paskutinis'.", + "limit-required": "Riba yra privaloma.", + "limit-range": "Riba turi būti nuo 2 iki 1000.", + "time-unit-milliseconds": "Milisekundės", + "time-unit-seconds": "Sekundės", + "time-unit-minutes": "Minutės", + "time-unit-hours": "Valandos", + "time-unit-days": "Dienos", + "time-value-range": "Leidžiamas diapazonas nuo 1 iki 2147483647.", + "start-interval-value-required": "Intervalo pradžia yra privaloma.", + "end-interval-value-required": "Intervalo pabaiga yra privaloma.", + "filter": "Filtras", + "switch": "Perjungiklis", + "math-templatization-tooltip": "Šis laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "add-message-type": "Pridėti pranešimo tipą", + "select-message-types-required": "Turi būti pasirinktas bent vienas pranešimo tipas.", + "select-message-types": "Pasirinkti pranešimo tipus", + "no-message-types-found": "Pranešimo tipų nerasta", + "no-message-type-matching": "'{{messageType}}' nerasta.", + "create-new-message-type": "Sukurti naują.", + "message-types-required": "Pranešimo tipai yra privalomi.", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendri atributai", + "server-attributes": "Serverio atributai", + "attributes-keys": "Atributų raktai", + "attributes-keys-required": "Atributų raktai yra privalomi", + "attributes-scope": "Atributų sritis", + "attributes-scope-value": "Atributų srities reikšmė", + "attributes-scope-value-copy": "Kopijuoti atributų srities reikšmę", + "attributes-scope-hint": "Naudokite metaduomenų raktą 'scope', kad dinamiškai nustatytumėte atributų sritį kiekvienam pranešimui. Jei pateikta, tai perrašo konfigūracijoje nustatytą sritį.", + "notify-device": "Priverstinai pranešti įrenginiui", + "send-attributes-updated-notification": "Siųsti atnaujintų atributų pranešimą", + "send-attributes-updated-notification-hint": "Siųsti pranešimą apie atnaujintus atributus kaip atskirą žinutę į taisyklių variklio eilę.", + "send-attributes-deleted-notification": "Siųsti ištrintų atributų pranešimą", + "send-attributes-deleted-notification-hint": "Siųsti pranešimą apie ištrintus atributus kaip atskirą žinutę į taisyklių variklio eilę.", + "update-attributes-only-on-value-change": "Išsaugoti atributus tik jei pasikeitė reikšmė", + "update-attributes-only-on-value-change-hint": "Atnaujina atributus su kiekvienu gaunamu pranešimu, neatsižvelgiant į tai, ar reikšmė pasikeitė. Tai padidina API apkrovą ir sumažina našumą.", + "update-attributes-only-on-value-change-hint-enabled": "Atnaujina atributus tik jei jų reikšmė pasikeitė. Jei reikšmė nepasikeitė, atributų laiko žyma ir pranešimas apie pakeitimą nebus siunčiami.", + "fetch-credentials-to-metadata": "Įtraukti prisijungimo duomenis į metaduomenis", + "notify-device-on-update-hint": "Jei įjungta, priverstinai praneša įrenginiui apie bendrų atributų atnaujinimą. Jei išjungta, pranešimo elgesį valdo 'notifyDevice' parametras iš gaunamo pranešimo metaduomenų. Norėdami išjungti pranešimą, pranešimo metaduomenyse turi būti parametras 'notifyDevice' nustatytas į 'false'. Kitais atvejais pranešimas bus siunčiamas įrenginiui.", + "notify-device-on-delete-hint": "Jei įjungta, priverstinai praneša įrenginiui apie bendrų atributų pašalinimą. Jei išjungta, pranešimo elgesį valdo 'notifyDevice' parametras iš gaunamo pranešimo metaduomenų. Norėdami įjungti pranešimą, pranešimo metaduomenyse turi būti parametras 'notifyDevice' nustatytas į 'true'. Kitais atvejais pranešimas įrenginiui nebus siunčiamas.", + "latest-timeseries": "Naujausių laiko eilučių duomenų raktai", + "timeseries-keys": "Laiko eilučių raktai", + "timeseries-keys-required": "Turi būti pasirinktas bent vienas laiko eilučių raktas.", + "add-timeseries-key": "Pridėti laiko eilučių raktą", + "add-message-field": "Pridėti pranešimo lauką", + "relation-search-parameters": "Ryšių paieškos parametrai", + "relation-parameters": "Ryšių parametrai", + "add-metadata-field": "Pridėti metaduomenų lauką", + "data-keys": "Pranešimo laukų pavadinimai", + "copy-from": "Kopijuoti iš", + "data-to-metadata": "Duomenis į metaduomenis", + "metadata-to-data": "Metaduomenis į duomenis", + "use-regular-expression-hint": "Naudokite reguliariąją išraišką raktų kopijavimui pagal šabloną.\n\nPatarimai:\nPaspauskite 'Enter', kad užbaigtumėte lauko pavadinimo įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte lauko pavadinimą. Palaikomi keli pavadinimai.", + "interval": "Intervalas", + "interval-required": "Intervalas yra privalomas", + "interval-hint": "Dublikavimo intervalas sekundėmis.", + "interval-min-error": "Mažiausia leidžiama reikšmė yra 1", + "max-pending-msgs": "Maks. laukiančių pranešimų", + "max-pending-msgs-hint": "Maksimalus pranešimų skaičius, saugomas atmintyje kiekvienam unikaliam dublikavimo ID.", + "max-pending-msgs-required": "Maks. laukiančių pranešimų reikšmė yra privaloma", + "max-pending-msgs-max-error": "Didžiausia leidžiama reikšmė yra 1000", + "max-pending-msgs-min-error": "Mažiausia leidžiama reikšmė yra 1", + "max-retries": "Maksimalus bandymų skaičius", + "max-retries-required": "Maksimalus bandymų skaičius yra privalomas", + "max-retries-hint": "Didžiausias bandymų skaičius išsiųsti dublikuotus pranešimus į eilę. Tarp bandymų taikomas 10 sekundžių delsimas.", + "max-retries-max-error": "Didžiausia leidžiama reikšmė yra 100", + "max-retries-min-error": "Mažiausia leidžiama reikšmė yra 0", + "strategy": "Strategija", + "strategy-required": "Strategija yra privaloma", + "strategy-all-hint": "Grąžina visus pranešimus, gautus dublikavimo laikotarpiu, kaip vieną JSON masyvą, kur kiekvienas elementas turi objektą su 'msg' ir 'metadata' savybėmis.", + "strategy-first-hint": "Grąžina pirmą pranešimą, gautą dublikavimo laikotarpiu.", + "strategy-last-hint": "Grąžina paskutinį pranešimą, gautą dublikavimo laikotarpiu.", + "first": "Pirmas", + "last": "Paskutinis", + "all": "Visi", + "output-msg-type-hint": "Dublikavimo rezultato pranešimo tipas.", + "queue-name-hint": "Eilės pavadinimas, į kurią bus publikuojamas dublikavimo rezultatas.", + "keys": "Raktai", + "keys-required": "Raktai yra privalomi", + "rename-keys-in": "Pervadinti raktus", + "data": "Duomenys", + "message": "Pranešimas", + "metadata": "Metaduomenys", + "current-key-name": "Dabartinis rakto pavadinimas", + "key-name-required": "Rakto pavadinimas yra privalomas", + "new-key-name": "Naujas rakto pavadinimas", + "new-key-name-required": "Naujas rakto pavadinimas yra privalomas", + "metadata-keys": "Metaduomenų laukų pavadinimai", + "json-path-expression": "JSON kelio išraiška", + "json-path-expression-required": "JSON kelio išraiška yra privaloma", + "json-path-expression-hint": "JSONPath nurodo kelią į elementą arba elementų rinkinį JSON struktūroje. '$' reiškia šakninį objektą arba masyvą.", + "relations-query": "Ryšių užklausa", + "device-relations-query": "Įrenginio ryšių užklausa", + "max-relation-level": "Didžiausias ryšių lygis", + "max-relation-level-error": "Reikšmė turi būti didesnė nei 0 arba nenustatyta.", + "max-relation-level-invalid": "Reikšmė turi būti sveikasis skaičius.", + "relation-type": "Ryšio tipas", + "relation-type-pattern": "Ryšio tipo šablonas", + "relation-type-pattern-required": "Ryšio tipo šablonas yra privalomas", + "relation-types-list": "Ryšių tipai, kuriuos reikia perduoti", + "relation-types-list-hint": "Jei nepasirinkti ryšių tipai, signalai (alarmai) bus perduodami be filtravimo pagal ryšio tipą.", + "unlimited-level": "Neribotas lygis", + "latest-telemetry": "Naujausia telemetrija", + "add-telemetry-key": "Pridėti telemetrijos raktą", + "delete-from": "Ištrinti iš", + "use-regular-expression-delete-hint": "Naudokite reguliariąją išraišką raktų ištrynimui pagal šabloną.\n\nPatarimai:\nPaspauskite 'Enter', kad užbaigtumėte lauko pavadinimo įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte lauko pavadinimą.\nPalaikomi keli pavadinimai.", + "fetch-into": "Užkrauti į", + "attr-mapping": "Atributų susiejimas:", + "source-attribute": "Pirminio atributo raktas", + "source-attribute-required": "Pirminio atributo raktas yra privalomas.", + "source-telemetry": "Pirminės telemetrijos raktas", + "source-telemetry-required": "Pirminės telemetrijos raktas yra privalomas.", + "target-key": "Tikslinis raktas", + "target-key-required": "Tikslinis raktas yra privalomas.", + "attr-mapping-required": "Turi būti nurodyta bent viena susiejimo eilutė.", + "fields-mapping": "Laukų susiejimas", + "fields-mapping-hint": "Jei pranešimo laukas nustatytas į $entityId, pranešimo kilmės (originatoriaus) ID bus įrašytas į nurodytą lentelės stulpelį.", + "relations-query-config-direction-suffix": "kilmės objektas", + "profile-name": "Profilio pavadinimas", + "fetch-circle-parameter-info-from-metadata-hint": "Metaduomenų laukas '{{perimeterKeyName}}' turi būti pateiktas šiuo formatu: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "Metaduomenų laukas '{{perimeterKeyName}}' turi būti pateiktas šiuo formatu: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "fields-mapping-required": "Turi būti nurodytas bent vienas laukų susiejimas.", + "at-least-one-field-required": "Bent vienas įvesties laukas turi turėti reikšmę (-es).", + "originator-fields-sv-map-hint": "Tikslinių raktų laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "sv-map-hint": "Tik tikslinių raktų laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "source-field": "Pirminis laukas", + "source-field-required": "Pirminis laukas yra privalomas.", + "originator-source": "Kilmės šaltinis", + "new-originator": "Naujas kilmės objektas", + "originator-customer": "Klientas", + "originator-tenant": "Nuomininkas", + "originator-related": "Susijęs objektas", + "originator-alarm-originator": "Signalo kilmės objektas", + "originator-entity": "Objektas pagal pavadinimo šabloną", + "clone-message": "Klonuoti pranešimą", + "transform": "Transformuoti", + "default-ttl": "Numatytasis TTL", + "default-ttl-required": "Numatytasis TTL yra privalomas.", + "default-ttl-hint": "Taisyklės mazgas (Rule Node) paims Time-to-Live (TTL) reikšmę iš pranešimo metaduomenų. Jei reikšmės nėra, bus naudojamas konfigūracijoje nurodytas TTL. Jei reikšmė lygi 0, bus taikomas TTL iš nuomininko (Tenant) profilio konfigūracijos.", + "default-ttl-zero-hint": "TTL nebus taikomas, jei jo reikšmė nustatyta į 0.", + "min-default-ttl-message": "Leidžiama tik minimali 0 TTL reikšmė.", + "generation-parameters": "Generavimo parametrai", + "message-count": "Generuojamų pranešimų limitas (0 - neribota)", + "message-count-required": "Generuojamų pranešimų limitas yra privalomas.", + "min-message-count-message": "Leidžiama tik minimali 0 pranešimų reikšmė.", + "period-seconds": "Periodas sekundėmis", + "period-seconds-required": "Periodas yra privalomas.", + "generation-frequency-seconds": "Generavimo dažnis sekundėmis", + "generation-frequency-required": "Generavimo dažnis yra privalomas.", + "min-generation-frequency-message": "Leidžiamas mažiausias 60 sekundžių intervalas.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "Naudoti periodo sekundėmis šabloną", + "use-metadata-period-in-seconds-patterns-hint": "Jei pasirinkta, taisyklės mazgas naudos periodo sekundėmis šabloną iš pranešimo metaduomenų arba duomenų, laikydamas, kad intervalai pateikti sekundėmis.", + "period-in-seconds-pattern": "Periodo sekundėmis šablonas", + "period-in-seconds-pattern-required": "Periodo sekundėmis šablonas yra privalomas", + "min-period-seconds-message": "Leidžiamas mažiausias 60 sekundžių periodas.", + "originator": "Kilmės objektas", + "message-body": "Pranešimo turinys", + "message-metadata": "Pranešimo metaduomenys", + "generate": "Generuoti", + "current-rule-node": "Dabartinis taisyklės mazgas", + "current-tenant": "Dabartinis nuomininkas", + "generator-function": "Generatoriaus funkcija", + "test-generator-function": "Testuoti generatoriaus funkciją", + "generator": "Generatorius", + "test-filter-function": "Testuoti filtravimo funkciją", + "test-switch-function": "Testuoti perjungimo funkciją", + "test-transformer-function": "Testuoti transformavimo funkciją", + "transformer": "Transformatorius", + "alarm-create-condition": "Signalo (aliarmo) sukūrimo sąlyga", + "test-condition-function": "Testuoti sąlygos funkciją", + "alarm-clear-condition": "Signalo (aliarmo) išvalymo sąlyga", + "alarm-details-builder": "Signalo (aliarmo) informacijos kūrėjas", + "test-details-function": "Testuoti informacijos kūrimo funkciją", + "alarm-type": "Signalo tipas", + "select-entity-types": "Pasirinkti objektų tipus", + "alarm-type-required": "Signalo tipas yra privalomas.", + "alarm-severity": "Signalo svarba", + "alarm-severity-required": "Signalo svarba yra privaloma", + "alarm-severity-pattern": "Signalo svarbos šablonas", + "alarm-status-filter": "Signalo būsenos filtras", + "alarm-status-list-empty": "Signalo būsenų sąrašas tuščias", + "no-alarm-status-matching": "Nerasta atitinkančių signalo būsenų.", + "propagate": "Perduoti signalą susijusiems objektams", + "propagate-to-owner": "Perduoti signalą objekto savininkui (klientui arba nuomininkui)", + "propagate-to-tenant": "Perduoti signalą nuomininkui", + "condition": "Sąlyga", + "details": "Išsami informacija", + "to-string": "Į eilutę (string)", + "test-to-string-function": "Testuoti konvertavimo į eilutę funkciją", + "from-template": "Iš", + "from-template-required": "Laukas „Iš“ yra privalomas", + "message-to-metadata": "Pranešimą į metaduomenis", + "metadata-to-message": "Metaduomenis į pranešimą", + "from-message": "Iš pranešimo", + "from-metadata": "Iš metaduomenų", + "to-template": "Į", + "to-template-required": "Laukas „Į šabloną“ yra privalomas", + "mail-address-list-template-hint": "Adreso sąrašas atskiriamas kableliais. Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio gauti.", + "cc-template": "Kopija (Cc)", + "bcc-template": "Slapta kopija (Bcc)", + "subject-template": "Tema", + "subject-template-required": "Temos šablonas yra privalomas", + "body-template": "Turinys", + "body-template-required": "Turinio šablonas yra privalomas", + "dynamic-mail-body-type": "Dinaminio el. laiško turinio tipas", + "mail-body-type": "El. laiško turinio tipas", + "body-type-template": "Turinio tipo šablonas", + "reply-routing-configuration": "Atsakymų nukreipimo konfigūracija", + "rpc-reply-routing-configuration-hint": "Šie konfigūracijos parametrai nurodo metaduomenų raktų pavadinimus, naudojamus paslaugos, sesijos ir užklausos identifikavimui siunčiant atsakymą atgal.", + "reply-routing-configuration-hint": "Šie konfigūracijos parametrai nurodo metaduomenų raktų pavadinimus, naudojamus paslaugos ir užklausos identifikavimui siunčiant atsakymą atgal.", + "request-id-metadata-attribute": "Užklausos ID", + "service-id-metadata-attribute": "Paslaugos ID", + "session-id-metadata-attribute": "Sesijos ID", + "timeout-sec": "Laiko limitas (sekundėmis)", + "timeout-required": "Laiko limitas yra privalomas", + "min-timeout-message": "Leidžiama tik minimali 0 laiko limito reikšmė.", + "endpoint-url-pattern": "Galutinio taško (endpoint) URL šablonas", + "endpoint-url-pattern-required": "Galutinio taško (endpoint) URL šablonas yra privalomas", + "request-method": "Užklausos metodas", + "use-simple-client-http-factory": "Naudoti paprastą HTTP kliento kūrimo būdą", + "ignore-request-body": "Be užklausos turinio", + "parse-to-plain-text": "Konvertuoti į gryną tekstą", + "parse-to-plain-text-hint": "Jei pasirinkta, užklausos turinys bus konvertuotas iš JSON eilutės į paprastą tekstą, pvz., msg = \"Hello,\\t\"world\"\" bus konvertuota į Hello, \"world\"", + "read-timeout": "Skaitymo laiko limitas (milisekundėmis)", + "read-timeout-hint": "Reikšmė 0 reiškia neribotą laiko limitą", + "max-parallel-requests-count": "Maksimalus lygiagrečių užklausų skaičius", + "max-parallel-requests-count-hint": "Reikšmė 0 nurodo, kad lygiagretus apdorojimas nėra ribojamas", + "max-response-size": "Maksimalus atsako dydis (KB)", + "max-response-size-hint": "Didžiausias atminties kiekis, skirtas duomenų buferiavimui dekoduojant ar koduojant HTTP pranešimus, pvz., JSON ar XML turinius", + "headers": "Antraštės (Headers)", + "headers-hint": "Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio antraščių/vertės laukuose", + "header": "Antraštė", + "header-required": "Antraštė yra privaloma", + "value": "Reikšmė", + "value-required": "Reikšmė yra privaloma", + "topic-pattern": "Temos šablonas", + "key-pattern": "Rakto šablonas", + "key-pattern-hint": "Nebūtina. Jei nurodytas galiojantis skaidinio (partition) numeris, jis bus naudojamas siunčiant įrašą. Jei skaidinys nenurodytas, bus naudojamas raktas. Jei abu nėra nurodyti – bus priskiriama apvaliuoju būdu (round-robin).", + "topic-pattern-required": "Temos šablonas yra privalomas", + "topic": "Tema", + "topic-required": "Tema yra privaloma", + "bootstrap-servers": "Bootstrap serveriai", + "bootstrap-servers-required": "Bootstrap serverių reikšmė yra privaloma", + "other-properties": "Kitos savybės", + "key": "Raktas", + "key-required": "Raktas yra privalomas", + "retries": "Automatinio bandymo kartai, jei nepavyksta", + "min-retries-message": "Leidžiama tik minimali 0 pakartojimų reikšmė.", + "batch-size-bytes": "Siunčiamo paketo dydis baitais", + "min-batch-size-bytes-message": "Leidžiama tik minimali 0 paketo dydžio reikšmė.", + "linger-ms": "Buferiavimo laikas vietoje (ms)", + "min-linger-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "buffer-memory-bytes": "Maksimalus kliento buferio dydis baitais", + "min-buffer-memory-message": "Leidžiama tik minimali 0 buferio dydžio reikšmė.", + "memory-buffer-size-range": "Atminties buferio dydis turi būti nuo 0 iki {{max}} KB", + "acks": "Patvirtinimų skaičius (ACKs)", + "topic-arn-pattern": "Temos ARN šablonas", + "topic-arn-pattern-required": "Temos ARN šablonas yra privalomas", + "aws-access-key-id": "AWS prieigos rakto ID (Access Key ID)", + "aws-access-key-id-required": "AWS prieigos rakto ID yra privalomas", + "aws-secret-access-key": "AWS slaptasis prieigos raktas (Secret Access Key)", + "aws-secret-access-key-required": "AWS slaptasis prieigos raktas yra privalomas", + "aws-region": "AWS regionas", + "aws-region-required": "AWS regionas yra privalomas", + "exchange-name-pattern": "Mainų (Exchange) pavadinimo šablonas", + "routing-key-pattern": "Maršruto rakto (Routing key) šablonas", + "message-properties": "Pranešimo savybės", + "host": "Serveris (Host)", + "host-required": "Serverio adresas yra privalomas", + "port": "Prievadas (Port)", + "port-required": "Prievadas yra privalomas", + "port-range": "Prievadas turi būti nuo 1 iki 65535.", + "virtual-host": "Virtualus serveris (Virtual host)", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "automatic-recovery": "Automatinis atkūrimas", + "connection-timeout-ms": "Prisijungimo laiko limitas (ms)", + "min-connection-timeout-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "handshake-timeout-ms": "Rankos paspaudimo (Handshake) laiko limitas (ms)", + "min-handshake-timeout-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "client-properties": "Kliento savybės", + "queue-url-pattern": "Eilės (Queue) URL šablonas", + "queue-url-pattern-required": "Eilės URL šablonas yra privalomas", + "delay-seconds": "Vėlinimas (sekundėmis)", + "min-delay-seconds-message": "Leidžiama tik minimali 0 sekundžių reikšmė.", + "max-delay-seconds-message": "Leidžiama tik maksimali 900 sekundžių reikšmė.", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas", + "queue-type": "Eilės tipas", + "sqs-queue-standard": "Standartinė", + "sqs-queue-fifo": "FIFO (First-In-First-Out)", + "gcp-project-id": "GCP projekto ID", + "gcp-project-id-required": "GCP projekto ID yra privalomas", + "gcp-service-account-key": "GCP paslaugos paskyros raktas (key file)", + "gcp-service-account-key-required": "GCP paslaugos paskyros raktas yra privalomas", + "pubsub-topic-name": "Temos pavadinimas", + "pubsub-topic-name-required": "Temos pavadinimas yra privalomas", + "message-attributes": "Pranešimo atributai", + "message-attributes-hint": "Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio nurodyti vardų/vertės laukuose", + "connect-timeout": "Prisijungimo laiko limitas (sekundėmis)", + "connect-timeout-required": "Prisijungimo laiko limitas yra privalomas.", + "connect-timeout-range": "Prisijungimo laiko limitas turi būti nuo 1 iki 200.", + "client-id": "Kliento ID", + "client-id-hint": "Nebūtina. Palikite tuščią, jei ID generuojamas automatiškai. Nurodydami konkretų Client ID būkite atsargūs — dauguma MQTT brokerių neleidžia kelių jungčių su tuo pačiu ID. Jei platforma veikia mikroservisų režimu, kiekvienas servisas paleidžia atskirą šio mazgo (rule node) egzempliorių, todėl dubliuoti ID gali sukelti klaidų. Kad to išvengtumėte, įjunkite „Pridėti paslaugos ID kaip priesagą prie kliento ID“.", + "append-client-id-suffix": "Pridėti paslaugos ID kaip priesagą prie kliento ID", + "client-id-suffix-hint": "Nebūtina. Naudojama, kai „Client ID“ nurodytas rankiniu būdu. Pridėjus paslaugos ID kaip priesagą, išvengiama klaidų mikroservisų architektūroje.", + "device-id": "Įrenginio ID", + "device-id-required": "Įrenginio ID yra privalomas.", + "clean-session": "Švari sesija (Clean session)", + "enable-ssl": "Įjungti SSL", + "credentials": "Autentifikacijos duomenys", + "credentials-type": "Autentifikacijos tipas", + "credentials-type-required": "Autentifikacijos tipas yra privalomas.", + "credentials-anonymous": "Anoniminis", + "credentials-basic": "Paprastas (Basic)", + "credentials-pem": "PEM sertifikatai", + "credentials-pem-hint": "Reikalingas bent Serverio CA sertifikatas arba Kliento sertifikato ir privačiojo rakto pora", + "credentials-sas": "Bendro prieigos parašo (Shared Access Signature)", + "sas-key": "SAS raktas", + "sas-key-required": "SAS raktas yra privalomas.", + "hostname": "Serverio pavadinimas (Hostname)", + "hostname-required": "Serverio pavadinimas yra privalomas.", + "azure-ca-cert": "CA sertifikato failas", + "username-required": "Vartotojo vardas yra privalomas.", + "password-required": "Slaptažodis yra privalomas.", + "ca-cert": "Serverio CA sertifikato failas", + "private-key": "Kliento privataus rakto failas", + "cert": "Kliento sertifikato failas", + "no-file": "Failas nepasirinktas.", + "drop-file": "Nutempkite failą arba spustelėkite, kad pasirinktumėte failą įkėlimui.", + "private-key-password": "Privataus rakto slaptažodis", + "use-system-smtp-settings": "Naudoti sistemos SMTP nustatymus", + "use-metadata-dynamic-interval": "Naudoti dinaminį intervalą", + "metadata-dynamic-interval-hint": "Intervalo pradžios ir pabaigos laukai palaiko šablonizaciją (templatization). Pakeistos reikšmės turi būti milisekundėmis. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "use-metadata-interval-patterns-hint": "Jei pasirinkta, mazgas (rule node) naudos pradžios ir pabaigos intervalo šablonus iš pranešimo duomenų arba metaduomenų, manydamas, kad intervalai yra milisekundėmis.", + "use-message-alarm-data": "Naudoti pranešimo pavojaus (alarm) duomenis", + "overwrite-alarm-details": "Perrašyti pavojaus detales", + "use-alarm-severity-pattern": "Naudoti pavojaus svarbos (severity) šabloną", + "check-all-keys": "Patikrinti, ar visi nurodyti laukai egzistuoja", + "check-all-keys-hint": "Jei pasirinkta, tikrina, ar visi nurodyti raktai yra pranešimo duomenyse ir metaduomenyse.", + "check-relation-to-specific-entity": "Tikrinti ryšį su konkrečiu objektu", + "check-relation-to-specific-entity-tooltip": "Jei įjungta, tikrinamas ryšio buvimas su konkrečiu objektu, kitu atveju – su bet kuriuo objektu. Abiem atvejais ryšys nustatomas pagal kryptį ir tipą.", + "check-relation-hint": "Tikrina ryšio buvimą su konkrečiu ar bet kuriuo objektu, atsižvelgiant į kryptį ir ryšio tipą.", + "delete-relation-with-specific-entity": "Pašalinti ryšį su konkrečiu objektu", + "delete-relation-with-specific-entity-hint": "Jei įjungta, bus pašalintas tik ryšys su vienu konkrečiu objektu. Kitu atveju, ryšys bus pašalintas su visais atitinkančiais objektais.", + "delete-relation-hint": "Pašalina ryšį tarp pranešimo kilmės (originator) ir nurodyto objekto (-ų), remiantis kryptimi ir tipu.", + "remove-current-relations": "Pašalinti esamus ryšius", + "remove-current-relations-hint": "Pašalina esamus ryšius iš pranešimo kilmės (originator) pagal kryptį ir tipą.", + "change-originator-to-related-entity": "Pakeisti pranešimo kilmę į susijusį objektą", + "change-originator-to-related-entity-hint": "Naudojama, kad pranešimas būtų apdorojamas kaip gautas iš kito objekto.", + "start-interval": "Intervalo pradžia", + "end-interval": "Intervalo pabaiga", + "start-interval-required": "Intervalo pradžia yra privaloma.", + "end-interval-required": "Intervalo pabaiga yra privaloma.", + "smtp-protocol": "Protokolas", + "smtp-host": "SMTP serveris", + "smtp-host-required": "SMTP serveris yra privalomas.", + "smtp-port": "SMTP prievadas", + "smtp-port-required": "Reikia nurodyti SMTP prievadą.", + "smtp-port-range": "SMTP prievadas turi būti nuo 1 iki 65535.", + "timeout-msec": "Laiko limitas (ms)", + "min-timeout-msec-message": "Leidžiama tik minimali 0 ms reikšmė.", + "enter-username": "Įveskite vartotojo vardą", + "enter-password": "Įveskite slaptažodį", + "enable-tls": "Įjungti TLS", + "tls-version": "TLS versija", + "enable-proxy": "Įjungti tarpiklį (Proxy)", + "use-system-proxy-properties": "Naudoti sistemos tarpiklio nustatymus", + "proxy-host": "Tarpiklio serveris (Proxy host)", + "proxy-host-required": "Tarpiklio serveris yra privalomas.", + "proxy-port": "Tarpiklio prievadas (Proxy port)", + "proxy-port-required": "Tarpiklio prievadas yra privalomas.", + "proxy-port-range": "Tarpiklio prievadas turi būti nuo 1 iki 65535.", + "proxy-user": "Tarpiklio vartotojas", + "proxy-password": "Tarpiklio slaptažodis", + "proxy-scheme": "Tarpiklio schema (Proxy scheme)", + "numbers-to-template": "Telefono numerių šablonas", + "numbers-to-template-required": "Telefono numerių šablonas yra privalomas", + "numbers-to-template-hint": "Telefono numerius atskirkite kableliais. Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio.", + "sms-message-template": "SMS žinutės šablonas", + "sms-message-template-required": "SMS žinutės šablonas yra privalomas", + "use-system-sms-settings": "Naudoti sistemos SMS paslaugų teikėjo nustatymus", + "min-period-0-seconds-message": "Leidžiamas tik minimalus 0 sekundžių periodas.", + "max-pending-messages": "Maksimalus laukiančių pranešimų skaičius", + "max-pending-messages-required": "Maksimalus laukiančių pranešimų skaičius yra privalomas.", + "max-pending-messages-range": "Maksimalus laukiančių pranešimų skaičius turi būti nuo 1 iki 100000.", + "originator-types-filter": "Siuntėjų (originatorių) tipų filtras", + "interval-seconds": "Intervalas sekundėmis", + "interval-seconds-required": "Intervalas yra privalomas.", + "int-range": "Reikšmė neturi viršyti maksimalios sveiko skaičiaus ribos (2147483648)", + "min-interval-seconds-message": "Leidžiamas tik minimalus 1 sekundės intervalas.", + "output-timeseries-key-prefix": "Išvesties laiko eilučių rakto priešdėlis", + "output-timeseries-key-prefix-required": "Išvesties laiko eilučių rakto priešdėlis yra privalomas.", + "separator-hint": "Paspauskite „Enter“, kad užbaigtumėte lauko įvedimą.", + "select-details": "Pasirinkti detales", + "entity-details-id": "Id", + "entity-details-title": "Pavadinimas", + "entity-details-country": "Šalis", + "entity-details-state": "Valstija / regionas", + "entity-details-city": "Miestas", + "entity-details-zip": "Pašto kodas", + "entity-details-address": "Adresas", + "entity-details-address2": "Adresas 2", + "entity-details-additional_info": "Papildoma informacija", + "entity-details-phone": "Telefonas", + "entity-details-email": "El. paštas", + "email-sender": "El. pašto siuntėjas", + "fields-to-check": "Laukai, kuriuos reikia patikrinti", + "add-detail": "Pridėti detalę", + "check-all-keys-tooltip": "Jei įjungta, tikrina, ar visi pranešime ir metaduomenyse nurodyti laukai yra pranešime.", + "fields-to-check-hint": "Paspauskite „Enter“, kad užbaigtumėte lauko įvedimą. Palaikomi keli laukų pavadinimai.", + "entity-details-list-empty": "Reikia pasirinkti bent vieną detalę.", + "alarm-status": "Pavojaus būsena", + "alarm-required": "Turi būti pasirinkta bent viena pavojaus būsena.", + "no-entity-details-matching": "Nerasta atitinkančių objektų detalių.", + "custom-table-name": "Individualios lentelės pavadinimas", + "custom-table-name-required": "Lentelės pavadinimas yra privalomas", + "custom-table-hint": "Lentelė turi būti sukurta jūsų Cassandra klasteryje, jos pavadinimas turi prasidėti prefiksu „cs_tb_“, kad duomenys nebūtų įrašyti į bendras TB lenteles. Įveskite lentelės pavadinimą be „cs_tb_“ prefikso.", + "message-field": "Pranešimo laukas", + "message-field-required": "Pranešimo laukas yra privalomas.", + "table-col": "Lentelės stulpelis", + "table-col-required": "Lentelės stulpelis yra privalomas.", + "latitude-field-name": "Platumos lauko pavadinimas", + "longitude-field-name": "Ilgumos lauko pavadinimas", + "latitude-field-name-required": "Platumos lauko pavadinimas yra privalomas.", + "longitude-field-name-required": "Ilgumos lauko pavadinimas yra privalomas.", + "fetch-perimeter-info-from-metadata": "Gauti perimetro informaciją iš metaduomenų", + "fetch-perimeter-info-from-metadata-tooltip": "Jei perimetro tipas nustatytas kaip „Poligonas“, metaduomenų lauko '{{perimeterKeyName}}' reikšmė bus naudojama kaip perimetro apibrėžimas be papildomo reikšmės apdorojimo. Priešingu atveju, jei perimetro tipas nustatytas kaip „Apskritimas“, '{{perimeterKeyName}}' metaduomenų lauko reikšmė bus išanalizuota, kad būtų išgauti 'latitude', 'longitude', 'radius', 'radiusUnit' laukai, naudojami apskritimo perimetro apibrėžimui.", + "perimeter-key-name": "Perimetro rakto pavadinimas", + "perimeter-key-name-hint": "Metaduomenų lauko pavadinimas, kuriame yra perimetro informacija.", + "perimeter-key-name-required": "Perimetro rakto pavadinimas yra privalomas.", + "perimeter-circle": "Apskritimas", + "perimeter-polygon": "Poligonas", + "perimeter-type": "Perimetro tipas", + "circle-center-latitude": "Centro platuma", + "circle-center-latitude-required": "Centro platuma yra privaloma.", + "circle-center-longitude": "Centro ilguma", + "circle-center-longitude-required": "Centro ilguma yra privaloma.", + "range-unit-meter": "Metrai", + "range-unit-kilometer": "Kilometrai", + "range-unit-foot": "Pėdos", + "range-unit-mile": "Mylios", + "range-unit-nautical-mile": "Jūrmylės", + "range-units": "Atstumo vienetai", + "range-units-required": "Atstumo vienetai yra privalomi.", + "range": "Atstumas", + "range-required": "Atstumas yra privalomas.", + "polygon-definition": "Poligono apibrėžimas", + "polygon-definition-required": "Poligono apibrėžimas yra privalomas.", + "polygon-definition-hint": "Naudokite šį formatą rankiniam poligono apibrėžimui: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "Minimalus buvimo viduje laikas", + "min-inside-duration-value-required": "Minimalus buvimo viduje laikas yra privalomas", + "min-inside-duration-time-unit": "Minimalus buvimo viduje laiko vienetas", + "min-outside-duration": "Minimalus buvimo išorėje laikas", + "min-outside-duration-value-required": "Minimalus buvimo išorėje laikas yra privalomas", + "min-outside-duration-time-unit": "Minimalus buvimo išorėje laiko vienetas", + "tell-failure-if-absent": "Pranešti apie klaidą", + "tell-failure-if-absent-hint": "Jei bent vienas pasirinktas raktas neegzistuoja, siunčiama žinutė praneš apie „Klaida“.", + "get-latest-value-with-ts": "Gauti naujausias telemetrijos reikšmes su laiko žyma", + "get-latest-value-with-ts-hint": "Jei pasirinkta, naujausios telemetrijos reikšmės taip pat turės laiko žymą, pvz.: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "Ignoruoti tuščias eilutes", + "ignore-null-strings-hint": "Jei pasirinkta, taisyklės mazgas ignoruos objektų laukus su tuščia reikšme.", + "add-metadata-key-values-as-kafka-headers": "Pridėti metaduomenų raktų-reikšmių poras į Kafka antraštes", + "add-metadata-key-values-as-kafka-headers-hint": "Jei pasirinkta, raktų-reikšmių poros iš metaduomenų bus pridėtos prie siunčiamų įrašų antraščių kaip baitų masyvai su iš anksto nustatytu kodavimu.", + "charset-encoding": "Koduotė", + "charset-encoding-required": "Koduotė yra privaloma.", + "charset-us-ascii": "US-ASCII", + "charset-iso-8859-1": "ISO-8859-1", + "charset-utf-8": "UTF-8", + "charset-utf-16be": "UTF-16BE", + "charset-utf-16le": "UTF-16LE", + "charset-utf-16": "UTF-16", + "select-queue-hint": "Eilės pavadinimą galima pasirinkti iš sąrašo arba įvesti rankiniu būdu.", + "device-profile-node-hint": "Naudinga, jei turite trukmės ar pasikartojančias sąlygas, kad būtų užtikrintas aliarmo būsenos vertinimo tęstinumas.", + "persist-alarm-rules": "Išsaugoti aliarmo taisyklių būseną", + "persist-alarm-rules-hint": "Jei įjungta, taisyklių mazgas išsaugos apdorojimo būseną duomenų bazėje.", + "fetch-alarm-rules": "Atkurti aliarmo taisyklių būseną", + "fetch-alarm-rules-hint": "Jei įjungta, taisyklių mazgas atkūrimo metu atkurs apdorojimo būseną ir užtikrins, kad aliarmai būtų aktyvuojami net po serverio perkrovimo. Priešingu atveju būsena bus atkurta, kai atvyks pirmoji žinutė iš įrenginio.", + "input-value-key": "Įvesties reikšmės raktas", + "input-value-key-required": "Įvesties reikšmės raktas yra privalomas.", + "output-value-key": "Išvesties reikšmės raktas", + "output-value-key-required": "Išvesties reikšmės raktas yra privalomas.", + "number-of-digits-after-floating-point": "Skaičių kiekis po kablelio", + "number-of-digits-after-floating-point-range": "Skaičių kiekis po kablelio turi būti nuo 0 iki 15.", + "failure-if-delta-negative": "Pranešti apie klaidą, jei delta yra neigiama", + "failure-if-delta-negative-tooltip": "Taisyklių mazgas privers pranešimą žymėti kaip klaidą, jei delta reikšmė neigiama.", + "use-caching": "Naudoti kešavimą", + "use-caching-tooltip": "Taisyklių mazgas kaups \"{{inputValueKey}}\" reikšmę iš gaunamos žinutės, kad pagerintų našumą. Atkreipkite dėmesį, kad kešas nebus atnaujintas, jei reikšmė bus pakeista kitur.", + "add-time-difference-between-readings": "Pridėti laiko skirtumą tarp \"{{inputValueKey}}\" nuskaitymų", + "add-time-difference-between-readings-tooltip": "Jei įjungta, taisyklių mazgas pridės \"{{periodValueKey}}\" prie siunčiamos žinutės.", + "period-value-key": "Laikotarpio reikšmės raktas", + "period-value-key-required": "Laikotarpio reikšmės raktas yra privalomas.", + "general-pattern-hint": "Naudokite ${metadataKey} reikšmei iš metaduomenų, $[messageKey] reikšmei iš pranešimo kūno.", + "alarm-severity-pattern-hint": "Naudokite ${metadataKey} reikšmei iš metaduomenų, $[messageKey] reikšmei iš pranešimo kūno. Aliarmo rimtumas turi būti sistemos (CRITICAL, MAJOR ir t. t.)", + "output-node-name-hint": "Taisyklių mazgo pavadinimas atitinka pranešimo ryšio tipą ir naudojamas pranešimams perduoti kitiems taisyklių mazgams toje pačioje taisyklių grandinėje.", + "use-server-ts": "Naudoti serverio laiko žymą", + "use-server-ts-hint": "Naudoti serverio dabartinę laiko žymą laiko eilučių duomenims, kurie neturi aiškios laiko žymos. Tai padeda išlaikyti teisingą eiliškumą apdorojant žinutes iš kelių šaltinių arba kai jos atvyksta ne iš eilės.", + "kv-map-pattern-hint": "Visi įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "kv-map-single-pattern-hint": "Įvesties laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "shared-scope": "Bendras taikymo sritis", + "server-scope": "Serverio taikymo sritis", + "client-scope": "Kliento taikymo sritis", + "attribute-type": "Atributas", + "attribute-type-description": "Gauti atributo reikšmę iš duomenų bazės", + "attribute-type-result-description": "Išsaugoti rezultatą kaip subjekto atributą duomenų bazėje", + "constant-type": "Konstanta", + "constant-type-description": "Nustatyti pastovią reikšmę", + "time-series-type": "Laiko eilutė", + "time-series-type-description": "Gauti naujausią laiko eilučių reikšmę iš duomenų bazės", + "time-series-type-result-description": "Išsaugoti rezultatą kaip subjekto laiko eilutę duomenų bazėje", + "message-body-type": "Pranešimas", + "message-body-type-description": "Gauti argumento reikšmę iš gaunamo pranešimo", + "message-body-type-result-description": "Pridėti rezultatą prie siunčiamo pranešimo", + "message-metadata-type": "Metaduomenys", + "message-metadata-type-description": "Gauti argumento reikšmę iš gaunamo pranešimo metaduomenų", + "message-metadata-result-description": "Pridėti rezultatą prie siunčiamo pranešimo metaduomenų", + "argument-tile": "Argumentai", + "no-arguments-prompt": "Argumentai nesukonfigūruoti", + "result-title": "Rezultatas", + "functions-field-input": "Funkcijos", + "no-option-found": "Parinkčių nerasta", + "argument-source-field-input": "Šaltinis", + "argument-source-field-input-required": "Argumento šaltinis yra privalomas.", + "argument-key-field-input": "Raktas", + "argument-key-field-input-required": "Argumento raktas yra privalomas.", + "constant-value-field-input": "Pastovioji reikšmė", + "constant-value-field-input-required": "Pastovioji reikšmė yra privaloma.", + "attribute-scope-field-input": "Atributo taikymo sritis", + "attribute-scope-field-input-required": "Atributo taikymo sritis yra privaloma.", + "default-value-field-input": "Numatytoji reikšmė", + "type-field-input": "Tipas", + "type-field-input-required": "Tipas yra privalomas.", + "key-field-input": "Raktas", + "add-entity-type": "Pridėti subjekto tipą", + "add-device-profile": "Pridėti įrenginio profilį", + "key-field-input-required": "Raktas yra privalomas.", + "number-floating-point-field-input": "Skaičių kiekis po kablelio", + "number-floating-point-field-input-hint": "Naudokite 0, kad konvertuotumėte rezultatą į sveiką skaičių", + "add-to-message-field-input": "Pridėti prie pranešimo", + "add-to-metadata-field-input": "Pridėti prie metaduomenų", + "custom-expression-field-input": "Matematinė išraiška", + "custom-expression-field-input-required": "Matematinė išraiška yra privaloma", + "custom-expression-field-input-hint": "Nurodykite matematinę išraišką įvertinimui. Numatytoji išraiška parodo, kaip konvertuoti Fahrenheit į Celsijų", + "retained-message": "Išsaugotas", + "attributes-mapping": "Atributų susiejimas", + "latest-telemetry-mapping": "Naujausios telemetrijos susiejimas", + "add-mapped-attribute-to": "Pridėti susietus atributus į", + "add-mapped-latest-telemetry-to": "Pridėti susietą naujausią telemetriją į", + "add-mapped-fields-to": "Pridėti susietus laukus į", + "add-selected-details-to": "Pridėti pasirinktą informaciją į", + "clear-selected-types": "Išvalyti pasirinktus tipus", + "clear-selected-details": "Išvalyti pasirinktą informaciją", + "clear-selected-fields": "Išvalyti pasirinktus laukus", + "clear-selected-keys": "Išvalyti pasirinktus raktus", + "geofence-configuration": "Geozonos konfigūracija", + "coordinate-field-names": "Koordinačių laukų pavadinimai", + "coordinate-field-hint": "Taisyklių mazgas bando gauti nurodytus laukus iš pranešimo. Jei jų nėra, ieškos metaduomenyse.", + "presence-monitoring-strategy": "Buvimo stebėjimo strategija", + "presence-monitoring-strategy-on-first-message": "Pirmoje žinutėje", + "presence-monitoring-strategy-on-each-message": "Kiekvienoje žinutėje", + "presence-monitoring-strategy-on-first-message-hint": "Praneša buvimo būseną „Viduje“ arba „Išorėje“ pirmoje žinutėje, kai nuo ankstesnio būsenos „Įėjo“ arba „Išėjo“ atnaujinimo praėjo nustatytas minimalus laikas.", + "presence-monitoring-strategy-on-each-message-hint": "Praneša buvimo būseną „Viduje“ arba „Išorėje“ kiekvienoje žinutėje po būsenos „Įėjo“ arba „Išėjo“ atnaujinimo.", + "fetch-credentials-to": "Gauti kredencialus į", + "add-originator-attributes-to": "Pridėti siuntėjo atributus į", + "originator-attributes": "Siuntėjo atributai", + "fetch-latest-telemetry-with-timestamp": "Gauti naujausią telemetriją su laiko žyma", + "fetch-latest-telemetry-with-timestamp-tooltip": "Jei pasirinkta, naujausios telemetrijos reikšmės bus pridėtos prie siunčiamų metaduomenų su laiko žyma, pvz.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "Pranešti apie klaidą, jei trūksta bent vieno atributo", + "tell-failure-tooltip": "Jei bent vienas pasirinktas raktas neegzistuoja, siunčiama žinutė praneš apie „Klaida“.", + "created-time": "Sukūrimo laikas", + "chip-help": "Paspauskite 'Enter', kad užbaigtumėte {{inputName}} įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte {{inputName}}.\nPalaikomos kelios reikšmės.", + "detail": "informacija", + "field-name": "lauko pavadinimas", + "device-profile": "įrenginio profilis", + "entity-type": "subjekto tipas", + "message-type": "pranešimo tipas", + "timeseries-key": "laiko eilučių raktas", + "type": "Tipas", + "first-name": "Vardas", + "last-name": "Pavardė", + "label": "Etiketė", + "originator-fields-mapping": "Siuntėjo laukų susiejimas", + "add-mapped-originator-fields-to": "Pridėti susietus siuntėjo laukus į", + "fields": "Laukai", + "skip-empty-fields": "Praleisti tuščius laukus", + "skip-empty-fields-tooltip": "Laukai su tuščiomis reikšmėmis nebus pridėti prie išvesties pranešimo ar metaduomenų.", + "fetch-interval": "Gavimo intervalas", + "fetch-strategy": "Gavimo strategija", + "fetch-timeseries-from-to": "Gauti laiko eilučių duomenis nuo {{startInterval}} {{startIntervalTimeUnit}} iki {{endInterval}} {{endIntervalTimeUnit}} atgal.", + "fetch-timeseries-from-to-invalid": "Laiko eilučių gavimo klaida („Intervalo pradžia“ turi būti mažesnė už „Intervalo pabaigą“).", + "use-metadata-dynamic-interval-tooltip": "Jei pasirinkta, taisyklių mazgas naudos dinaminį intervalo pradžios ir pabaigos laiką pagal pranešimo bei metaduomenų šablonus.", + "all-mode-hint": "Jei pasirinktas gavimo režimas „Visi“, taisyklių mazgas gaus telemetriją iš intervalo pagal konfigūruojamus užklausos parametrus.", + "first-mode-hint": "Jei pasirinktas režimas „Pirmas“, taisyklių mazgas gaus artimiausią telemetriją intervalo pradžiai.", + "last-mode-hint": "Jei pasirinktas režimas „Paskutinis“, taisyklių mazgas gaus artimiausią telemetriją intervalo pabaigai.", + "ascending": "Didėjančia tvarka", + "descending": "Mažėjančia tvarka", + "min": "Min.", + "max": "Maks.", + "average": "Vidurkis", + "sum": "Suma", + "count": "Kiekis", + "none": "Nėra", + "last-level-relation-tooltip": "Jei pasirinkta, taisyklių mazgas ieškos susijusių subjektų tik maksimaliame nustatytame ryšio lygyje.", + "last-level-device-relation-tooltip": "Jei pasirinkta, taisyklių mazgas ieškos susijusių įrenginių tik maksimaliame nustatytame ryšio lygyje.", + "data-to-fetch": "Duomenys, kuriuos reikia gauti", + "mapping-of-customers": "Klientų susiejimas", + "map-fields-required": "Visi susiejimo laukai yra privalomi.", + "attributes": "Atributai", + "related-device-attributes": "Susijusio įrenginio atributai", + "add-selected-attributes-to": "Pridėti pasirinktus atributus į", + "device-profiles": "Įrenginių profiliai", + "mapping-of-tenant": "Nuomininko susiejimas", + "add-attribute-key": "Pridėti atributo raktą", + "message-template": "Pranešimo šablonas", + "message-template-required": "Pranešimo šablonas yra privalomas", + "use-system-slack-settings": "Naudoti sistemos Slack nustatymus", + "slack-api-token": "Slack API raktas", + "slack-api-token-required": "Slack API raktas yra privalomas", + "keys-mapping": "Raktų susiejimas", + "add-key": "Pridėti raktą", + "recipients": "Gavėjai", + "message-subject-and-content": "Pranešimo tema ir turinys", + "template-rules-hint": "Abu įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "originator-customer-desc": "Naudoti gaunamo pranešimo siuntėjo klientą kaip naują siuntėją.", + "originator-tenant-desc": "Naudoti dabartinį nuomininką kaip naują siuntėją.", + "originator-related-entity-desc": "Naudoti susijusį subjektą kaip naują siuntėją. Paieška atliekama pagal konfigūruotą ryšio tipą ir kryptį.", + "originator-alarm-originator-desc": "Naudoti aliarmo siuntėją kaip naują siuntėją. Tik jei gaunamo pranešimo siuntėjas yra aliarmo subjektas.", + "originator-entity-by-name-pattern-desc": "Naudoti iš duomenų bazės gautą subjektą kaip naują siuntėją. Paieška atliekama pagal subjekto tipą ir nurodytą pavadinimo šabloną.", + "email-from-template-hint": "Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "recipients-block-main-hint": "Adresų sąrašas, atskirtas kableliais. Visi įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "forward-msg-default-rule-chain": "Persiųsti pranešimą į siuntėjo numatytą taisyklių grandinę", + "forward-msg-default-rule-chain-tooltip": "Jei įjungta, pranešimas bus persiųstas į siuntėjo numatytą taisyklių grandinę arba į konfigūracijoje nurodytą grandinę, jei siuntėjas neturi numatytos grandinės subjekto profilyje.", + "exclude-zero-deltas": "Pašalinti nulinės delta reikšmes iš siunčiamo pranešimo", + "exclude-zero-deltas-hint": "Jei įjungta, \"{{outputValueKey}}\" bus pridėtas prie siunčiamo pranešimo tik jei reikšmė nėra lygi nuliui.", + "exclude-zero-deltas-time-difference-hint": "Jei įjungta, \"{{outputValueKey}}\" ir \"{{periodValueKey}}\" bus pridėti prie siunčiamo pranešimo tik jei \"{{outputValueKey}}\" reikšmė nėra nulinė.", + "search-direction-from": "Nuo siuntėjo iki tikslo", + "search-direction-to": "Nuo tikslo iki siuntėjo", + "del-relation-direction-from": "Nuo siuntėjo", + "del-relation-direction-to": "Į siuntėją", + "target-entity": "Tikslinis subjektas", + "function-configuration": "Funkcijos konfigūracija", + "function-name": "Funkcijos pavadinimas", + "function-name-required": "Funkcijos pavadinimas yra privalomas.", + "qualifier": "Kvalifikatorius", + "qualifier-hint": "Jei kvalifikatorius nenurodytas, bus naudojamas numatytasis \"$LATEST\".", + "aws-credentials": "AWS kredencialai", + "connection-timeout": "Ryšio laiko limitas", + "connection-timeout-required": "Ryšio laiko limitas yra privalomas.", + "connection-timeout-min": "Minimalus ryšio laikas yra 0.", + "connection-timeout-hint": "Laikas sekundėmis, kurį sistema laukia užmezgant ryšį prieš nutraukdama bandymą. Reikšmė 0 reiškia begalybę ir nerekomenduojama.", + "request-timeout": "Užklausos laiko limitas", + "request-timeout-required": "Užklausos laiko limitas yra privalomas.", + "request-timeout-min": "Minimalus užklausos laikas yra 0.", + "request-timeout-hint": "Laikas sekundėmis, kiek sistema laukia, kol užklausa bus įvykdyta. Reikšmė 0 reiškia begalybę ir nerekomenduojama.", + "units": "Vienetai", + "tell-failure-aws-lambda": "Pranešti apie klaidą, jei AWS Lambda funkcijos vykdymas sukelia išimtį", + "tell-failure-aws-lambda-hint": "Taisyklių mazgas privers pranešimą žymėti kaip klaidą, jei AWS Lambda funkcijos vykdymas sukelia išimtį.", + "basic-mode": "Paprastas režimas", + "advanced-mode": "Išplėstinis režimas", + "save-time-series": { + "processing-settings": "Apdorojimo nustatymai", + "processing-settings-hint": "Nustatykite, kaip apdorojamos gaunamos žinutės. Paprasti apdorojimo nustatymai leidžia pasirinkti iš anksto sukonfigūruotas strategijas, o išplėstiniai – individualiai pritaikyti strategijas kiekvienam veiksmui.", + "advanced-settings-hint": "Būkite atsargūs konfigūruodami apdorojimo strategijas. Kai kurie deriniai gali sukelti netikėtą elgesį.", + "strategy": "Strategija", + "deduplication-interval": "Dublikavimo šalinimo intervalas", + "deduplication-interval-required": "Dublikavimo šalinimo intervalas yra privalomas.", + "deduplication-interval-min-max-range": "Dublikavimo šalinimo intervalas turi būti ne trumpesnis nei 1 sekundė ir ne ilgesnis nei 1 diena.", + "strategy-type": { + "every-message": "Kiekvienai žinutei", + "skip": "Praleisti", + "deduplicate": "Pašalinti dublikatus", + "web-sockets-only": "Tik WebSockets" + }, + "time-series": "Laiko eilutės", + "latest": "Naujausios reikšmės", + "web-sockets": "WebSockets", + "calculated-fields": "Apskaičiuoti laukai" + }, + "save-attribute": { + "processing-settings": "Apdorojimo nustatymai", + "processing-settings-hint": "Nustatykite, kaip bus apdorojamos gaunamos žinutės. Paprasti apdorojimo nustatymai leidžia pasirinkti iš anksto sukonfigūruotas strategijas, o išplėstiniai – pasirinkti individualias apdorojimo strategijas kiekvienam veiksmui.", + "advanced-settings-hint": "Būkite atsargūs konfigūruodami apdorojimo strategijas. Kai kurie deriniai gali sukelti netikėtą elgesį.", + "strategy": "Strategija", + "deduplication-interval": "Dublikavimo šalinimo intervalas", + "deduplication-interval-required": "Dublikavimo šalinimo intervalas yra privalomas.", + "deduplication-interval-min-max-range": "Dublikavimo šalinimo intervalas turi būti ne trumpesnis nei 1 sekundė ir ne ilgesnis nei 1 diena.", + "scope": "Taikymo sritis", + "strategy-type": { + "every-message": "Kiekvienai žinutei", + "skip": "Praleisti", + "deduplicate": "Pašalinti dublikatus", + "web-sockets-only": "Tik WebSockets" + }, + "attributes": "Atributai" + }, + "key-val": { + "key": "Raktas", + "value": "Reikšmė", + "see-examples": "Žiūrėti pavyzdžius.", + "remove-entry": "Pašalinti įrašą", + "remove-mapping-entry": "Pašalinti susiejimo įrašą", + "add-mapping-entry": "Pridėti susiejimą", + "add-entry": "Pridėti įrašą", + "copy-key-values-from": "Kopijuoti raktų-reikšmių poras iš", + "delete-key-values": "Ištrinti raktų-reikšmių poras", + "delete-key-values-from": "Ištrinti raktų-reikšmių poras iš", + "at-least-one-key-error": "Turi būti pasirinktas bent vienas raktas.", + "unique-key-value-pair-error": "'{{keyText}}' turi skirtis nuo '{{valText}}'!" + }, + "mail-body-types": { + "plain-text": "Grynasis tekstas", + "html": "HTML", + "dynamic": "Dinaminis", + "use-body-type-template": "Naudoti turinio tipo šabloną", + "plain-text-description": "Paprastas, neformatuotas tekstas be specialaus stiliaus ar formatavimo.", + "html-text-description": "Leidžia naudoti HTML žymes formatavimui, nuorodoms ir vaizdams el. laiško turinyje.", + "dynamic-text-description": "Leidžia dinamiškai naudoti Grynąjį tekstą arba HTML turinio tipą pagal šablonizacijos funkciją.", + "after-template-evaluation-hint": "Po šablono įvertinimo reikšmė turi būti „true“ HTML turiniui ir „false“ grynajam tekstui." + }, + "ai": { + "ai-model": "DI modelis", + "model": "Modelis", + "ai-model-hint": "Pasirinkite iš anksto sukonfigūruotą DI modelį užklausų, siunčiamų per šį taisyklių mazgą, apdorojimui arba naudokite „Sukurti naują“, kad sukonfigūruotumėte naują modelį.", + "prompt-settings": "Užklausos nustatymai", + "prompt-settings-hint": "Neprivalomas sistemos užklausos tekstas nustato DI bendrą vaidmenį ir apribojimus, o vartotojo užklausa apibrėžia konkrečią vykdomą užduotį. Abu laukai palaiko šablonizaciją.", + "system-prompt": "Sistemos užklausa", + "system-prompt-max-length": "Sistemos užklausa turi būti ne ilgesnė nei 500000 simbolių.", + "system-prompt-blank": "Sistemos užklausa negali būti tuščia.", + "user-prompt": "Vartotojo užklausa", + "user-prompt-required": "Vartotojo užklausa yra privaloma.", + "user-prompt-max-length": "Vartotojo užklausa turi būti ne ilgesnė nei 500000 simbolių.", + "user-prompt-blank": "Vartotojo užklausa negali būti tuščia.", + "response-format": "Atsakymo formatas", + "response-text": "Tekstas", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-format-hint-TEXT": "Leidžia modeliui generuoti laisvo formato tekstą, kuris gali būti arba nebūti tinkamas JSON objektas. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-format-hint-JSON": "Modelis privalo sugeneruoti atsakymą, kuris yra teisingas JSON objektas. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-format-hint-JSON_SCHEMA": "Modelis privalo sugeneruoti JSON, atitinkantį nurodytą struktūrą ir duomenų tipus pagal pateiktą schemą. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-json-schema-hint": "Galima įvesti bet kokią tinkamą JSON schemą, tačiau šis taisyklių mazgas palaiko tik ribotą jos funkcionalumo rinkinį. Išsamesnę informaciją rasite mazgo dokumentacijoje.", + "response-json-schema-required": "JSON schema yra privaloma.", + "advanced-settings": "Išplėstiniai nustatymai", + "timeout": "Laiko limitas", + "timeout-hint": "Didžiausias laukimo laikas, kol bus gautas atsakymas iš DI modelio prieš nutraukiant užklausą.", + "timeout-required": "Laiko limitas yra privalomas.", + "timeout-validation": "Turi būti nuo 1 sekundės iki 10 minučių.", + "force-acknowledgement": "Priverstinis patvirtinimas", + "force-acknowledgement-hint": "Jei įjungta, gaunamas pranešimas patvirtinamas iš karto. Modelio atsakymas tada įtraukiamas į eilę kaip atskiras naujas pranešimas." + } + }, + "timezone": { + "timezone": "Laiko juosta", + "select-timezone": "Pasirinkti laiko juostą", + "no-timezones-matching": "Laiko juostų, atitinkančių '{{timezone}}' nėra.", + "timezone-required": "Laiko juosta būtina.", + "browser-time": "Naršyklės laikas" + }, + "queue": { + "queue-name": "Eilė", + "no-queues-found": "Eilių nerasta.", + "no-queues-matching": "Eilių, atitinkančių '{{queue}}', nerasta.", + "select-name": "Pasirinkite eilės pavadinimą", + "name": "Pavadinimas", + "name-required": "Eilės pavadinimas yra privalomas!", + "name-unique": "Eilės pavadinimas nėra unikalus!", + "name-pattern": "Eilės pavadinimas turi simbolį, kuris nėra ASCII raidinis-skaitmeninis, '.', '_' ar '-'!", + "queue-required": "Eilė yra privaloma!", + "topic-required": "Eilės tema yra privaloma!", + "poll-interval-required": "Apklausos intervalas yra privalomas!", + "poll-interval-min-value": "Apklausos intervalo reikšmė negali būti mažesnė nei 1.", + "partitions-required": "Padaliniai yra privalomi!", + "partitions-min-value": "Padalinių reikšmė negali būti mažesnė nei 1.", + "pack-processing-timeout-required": "Apdorojimo laiko limitas yra privalomas.", + "pack-processing-timeout-min-value": "Apdorojimo laiko limito reikšmė negali būti mažesnė nei 1.", + "batch-size-required": "Paketo dydis yra privalomas!", + "batch-size-min-value": "Paketo dydžio reikšmė negali būti mažesnė nei 1.", + "retries-required": "Bandymai yra privalomi!", + "retries-min-value": "Bandymai negali būti neigiami.", + "failure-percentage-required": "Klaidos procentas yra privalomas!", + "failure-percentage-min-value": "Klaidos procentas negali būti mažesnis nei 0.", + "failure-percentage-max-value": "Klaidos procentas negali būti didesnis nei 100.", + "pause-between-retries-required": "Pauzė tarp bandymų yra privaloma!", + "pause-between-retries-min-value": "Pauzės tarp bandymų reikšmė negali būti mažesnė nei 1.", + "max-pause-between-retries-required": "Maksimali pauzė tarp bandymų yra privaloma!", + "max-pause-between-retries-min-value": "Maksimalios pauzės tarp bandymų reikšmė negali būti mažesnė nei 1.", + "submit-strategy-type-required": "Pateikimo strategijos tipas yra privalomas!", + "processing-strategy-type-required": "Apdorojimo strategijos tipas yra privalomas!", + "queues": "Eilės", + "selected-queues": "Pasirinkta { count, plural, =1 {1 eilė} other {# eilės} }", + "delete-queue-title": "Ar tikrai norite ištrinti eilę '{{queueName}}'?", + "delete-queues-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 eilę} other {# eiles} }?", + "delete-queue-text": "Būkite atsargūs — po patvirtinimo eilė ir visi su ja susiję duomenys bus negrįžtamai pašalinti.", + "delete-queues-text": "Po patvirtinimo visos pasirinktos eilės bus ištrintos ir taps nepasiekiamos.", + "search": "Ieškoti eilės", + "add": "Pridėti eilę", + "details": "Eilės informacija", + "topic": "Tema", + "submit-settings": "Pateikimo nustatymai", + "submit-strategy": "Strategijos tipas *", + "grouping-parameter": "Grupavimo parametras", + "processing-settings": "Bandyminių apdorojimų nustatymai", + "processing-strategy": "Apdorojimo tipas *", + "retries-settings": "Bandyminių bandymų nustatymai", + "polling-settings": "Apklausos nustatymai", + "batch-processing": "Paketo apdorojimas", + "poll-interval": "Apklausos intervalas", + "partitions": "Padaliniai", + "immediate-processing": "Tiesioginis apdorojimas", + "consumer-per-partition": "Siųsti žinučių apklausą kiekvienam vartotojui", + "consumer-per-partition-hint": "Įjungti atskirą vartotoją (-us) kiekvienam padaliniui", + "duplicate-msg-to-all-partitions": "Dubliuoti žinutę į visus padalinius", + "processing-timeout": "Apdorojimo trukmė, ms", + "batch-size": "Paketo dydis", + "retries": "Bandymai (0 – neribota)", + "failure-percentage": "Klaidingų žinučių procentas, praleidžiant bandymus", + "pause-between-retries": "Pakartoti po, sek.", + "max-pause-between-retries": "Papildomas pakartojimas po, sek.", + "delete": "Ištrinti eilę", + "copyId": "Kopijuoti eilės ID", + "idCopiedMessage": "Eilės ID nukopijuotas į iškarpinę", + "description": "Aprašymas", + "description-hint": "Šis tekstas bus rodomas eilės aprašyme vietoj pasirinktos strategijos.", + "alt-description": "Pateikimo strategija: {{submitStrategy}}, Apdorojimo strategija: {{processingStrategy}}", + "custom-properties": "Pasirinktinės savybės", + "custom-properties-hint": "Pasirinktinės eilės (temos) kūrimo savybės, pvz.: 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "Sekvencinis pagal siuntėją", + "sequential-by-originator-hint": "Nauja žinutė, pvz., iš įrenginio A, nepateikiama, kol nepatvirtinama ankstesnė žinutė iš to paties įrenginio.", + "sequential-by-tenant-label": "Sekvencinis pagal nuomininką", + "sequential-by-tenant-hint": "Nauja žinutė, pvz., iš nuomininko A, nepateikiama, kol nepatvirtinama ankstesnė žinutė iš to paties nuomininko.", + "sequential-label": "Sekvencinis", + "sequential-hint": "Nauja žinutė nepateikiama, kol nepatvirtinama ankstesnė žinutė.", + "burst-label": "Srautinis (Burst)", + "burst-hint": "Visos žinutės pateikiamos taisyklių grandinėms tokia tvarka, kokia jos gaunamos.", + "batch-label": "Paketas", + "batch-hint": "Naujas paketas nepateikiamas, kol ankstesnis nepatvirtintas.", + "skip-all-failures-label": "Praleisti visas klaidas", + "skip-all-failures-hint": "Ignoruoti visas klaidas.", + "skip-all-failures-and-timeouts-label": "Praleisti visas klaidas ir laiko limitus", + "skip-all-failures-and-timeouts-hint": "Ignoruoti visas klaidas ir laiko viršijimus.", + "retry-all-label": "Kartoti visas", + "retry-all-hint": "Kartoti visas žinutes iš apdorojimo paketo.", + "retry-failed-label": "Kartoti nesėkmingas", + "retry-failed-hint": "Kartoti visas nesėkmingai apdorotas žinutes iš paketo.", + "retry-timeout-label": "Kartoti laiko viršijusias", + "retry-timeout-hint": "Kartoti visas laiko limitą viršijusias žinutes iš apdorojimo paketo.", + "retry-failed-and-timeout-label": "Kartoti nesėkmingas ir laiko viršijusias", + "retry-failed-and-timeout-hint": "Kartoti visas nesėkmingas ir laiko limitą viršijusias žinutes iš apdorojimo paketo." + } + }, + "queue-statistics": { + "queue-statistics": "Eilių statistika", + "no-queue-statistics-matching": "Eilių statistikos, atitinkančios '{{entity}}', nerasta.", + "queue-statistics-required": "Eilių statistika yra privaloma.", + "list-of-queue-statistics": "{ count, plural, =1 {Viena eilės statistika} other {# eilių statistikos sąrašas} }", + "selected-queue-statistics": "Pasirinkta { count, plural, =1 {1 eilės statistika} other {# eilių statistikos} }", + "no-queue-statistics-text": "Eilių statistikos nerasta", + "queue-statistics-starts-with": "Eilių statistika, kurios pavadinimai prasideda '{{prefix}}'" + }, + "server-error": { + "general": "Bendra serverio klaida", + "authentication": "Autentifikavimo klaida", + "jwt-token-expired": "JWT prieigos raktas pasibaigęs", + "tenant-trial-expired": "Nuomininko bandomasis laikotarpis pasibaigė", + "credentials-expired": "Prisijungimo duomenys pasibaigę", + "permission-denied": "Prieiga uždrausta", + "invalid-arguments": "Neteisingi argumentai", + "bad-request-params": "Netinkami užklausos parametrai", + "item-not-found": "Elementas nerastas", + "too-many-requests": "Per daug užklausų", + "too-many-updates": "Per daug atnaujinimų" + }, + "tenant": { + "tenant": "Valdytojas", + "tenants": "Valdytojai", + "management": "Valdytojų valdymas", + "add": "Pridėti valdytoją", + "admins": "Administratoriai", + "manage-tenant-admins": "Valdyti valdytojo administratorius", + "delete": "Ištrinti valdytoją", + "add-tenant-text": "Pridėti naują valdytoją", + "no-tenants-text": "Valdytojų nėra", + "tenant-details": "Informacija apie valdytoją", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "delete-tenant-title": "Ar tikrai norite ištrinti valdytoją '{{tenantTitle}}'?", + "delete-tenant-text": "Būkite dėmesingi, po patvirtinimo valdytojas ir visa su juo susijusi informacija bus pašalinta.", + "delete-tenants-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }?", + "delete-tenants-action-title": "Pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }", + "delete-tenants-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojai ir su jais susijusi informacija bus pašalinta.", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti valdytojo Id", + "idCopiedMessage": "Valdytojo Id nukopijuotas į iškarpinę", + "select-tenant": "Pasirinkti valdytoją", + "no-tenants-matching": "Valdytojų, atitinkančių '{{entity}}' nėra.", + "tenant-required": "Valdytojas būtinas", + "search": "Valdytojų paieška", + "selected-tenants": "Pasirinkta { count, plural, =1 {1 valdytojas} other {# valdytojai} }", + "isolated-tb-rule-engine": "Naudoti atskiras ThingsBoard taisyklių variklio eiles", + "isolated-tb-rule-engine-details": "Kiekvienas nuomininkas turės atskiras taisyklių variklio eiles" + }, + "tenant-profile": { + "tenant-profile": "Nuomininko profilis", + "tenant-profiles": "Nuomininkų profiliai", + "add": "Pridėti nuomininko profilį", + "add-profile": "Pridėti profilį", + "debug": "Derinimas", + "edit": "Redaguoti nuomininko profilį", + "tenant-profile-details": "Informacija apie nuomininko profilį", + "no-tenant-profiles-text": "Nuomininkų profilių nėra", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "search": "Nuomininkų profilių paieška", + "selected-tenant-profiles": "Pasirinkta { count, plural, =1 {1 nuomininko profilis} other {# nuomininkų profiliai} }", + "no-tenant-profiles-matching": "Nuomininkų profilių, atitinkančių '{{entity}}', nėra.", + "tenant-profile-required": "Nuomininko profilis yra privalomas", + "idCopiedMessage": "Nuomininko profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip pagrindinį nuomininko profilį", + "delete": "Pašalinti nuomininko profilį", + "copyId": "Kopijuoti nuomininko profilio ID", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "data": "Profilio duomenys", + "profile-configuration": "Profilio konfigūracija", + "description": "Aprašymas", + "default": "Pagrindinis", + "delete-tenant-profile-title": "Ar tikrai norite pašalinti nuomininko profilį '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Būkite atsargūs — po patvirtinimo nuomininko profilis ir visa su juo susijusi informacija bus negrįžtamai pašalinta.", + "delete-tenant-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 nuomininko profilį} other {# nuomininkų profilius} }?", + "delete-tenant-profiles-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti nuomininkų profiliai ir su jais susijusi informacija bus negrįžtamai pašalinti.", + "set-default-tenant-profile-title": "Ar tikrai norite nuomininko profilį '{{tenantProfileName}}' nustatyti kaip pagrindinį?", + "set-default-tenant-profile-text": "Po patvirtinimo šis nuomininko profilis bus nustatytas kaip pagrindinis ir bus automatiškai priskiriamas naujiems nuomininkams, kuriems profilis nenurodytas.", + "no-tenant-profiles-found": "Nuomininkų profilių nerasta.", + "create-new-tenant-profile": "Sukurti naują!", + "create-tenant-profile": "Sukurti naują nuomininko profilį", + "import": "Importuoti nuomininko profilį", + "export": "Eksportuoti nuomininko profilį", + "export-failed-error": "Nepavyko eksportuoti nuomininko profilio: {{error}}", + "tenant-profile-file": "Nuomininko profilio failas", + "invalid-tenant-profile-file-error": "Nepavyko importuoti nuomininko profilio: neteisinga duomenų struktūra.", + "advanced-settings": "Pažangūs nustatymai", + "entities": "Subjektai", + "rule-engine": "Taisyklių variklis", + "time-to-live": "Gyvavimo trukmė (Time-to-Live)", + "calculated-fields": "Apskaičiuojami laukai", + "alarms-and-notifications": "Pavojaus signalai ir pranešimai", + "ota-files-in-bytes": "OTA failai baitais", + "ws-title": "WS", + "unlimited": "(0 – neribota)", + "maximum-devices": "Didžiausias įrenginių skaičius", + "maximum-devices-required": "Didžiausias įrenginių skaičius yra privalomas.", + "maximum-devices-range": "Didžiausias įrenginių skaičius negali būti neigiamas", + "maximum-assets": "Didžiausias išteklių (asset) skaičius", + "maximum-assets-required": "Didžiausias išteklių (asset) skaičius yra privalomas.", + "maximum-assets-range": "Didžiausias išteklių (asset) skaičius negali būti neigiamas", + "maximum-customers": "Didžiausias klientų skaičius", + "maximum-customers-required": "Didžiausias klientų skaičius yra privalomas.", + "maximum-customers-range": "Didžiausias klientų skaičius negali būti neigiamas", + "maximum-users": "Didžiausias vartotojų skaičius", + "maximum-users-required": "Didžiausias vartotojų skaičius yra privalomas.", + "maximum-users-range": "Didžiausias vartotojų skaičius negali būti neigiamas", + "maximum-dashboards": "Didžiausias skydelių (dashboard) skaičius", + "maximum-dashboards-required": "Didžiausias skydelių (dashboard) skaičius yra privalomas.", + "maximum-dashboards-range": "Didžiausias skydelių (dashboard) skaičius negali būti neigiamas", + "maximum-edges": "Didžiausias Edge įrenginių skaičius", + "maximum-edges-required": "Didžiausias Edge įrenginių skaičius yra privalomas.", + "maximum-edges-range": "Didžiausias Edge įrenginių skaičius negali būti neigiamas", + "maximum-rule-chains": "Didžiausias taisyklių grandinių (rule chains) skaičius", + "maximum-rule-chains-required": "Didžiausias taisyklių grandinių (rule chains) skaičius yra privalomas.", + "maximum-rule-chains-range": "Didžiausias taisyklių grandinių (rule chains) skaičius negali būti neigiamas", + "maximum-resources-sum-data-size": "Išteklių failų bendras dydis", + "maximum-resources-sum-data-size-required": "Išteklių failų bendras dydis yra privalomas.", + "maximum-resources-sum-data-size-range": "Išteklių failų bendras dydis negali būti neigiamas", + "maximum-resource-size": "Didžiausias vieno ištekliaus failo dydis (baitais)", + "maximum-resource-size-required": "Didžiausias ištekliaus failo dydis yra privalomas.", + "maximum-resource-size-range": "Didžiausias ištekliaus failo dydis negali būti neigiamas", + "maximum-ota-packages-sum-data-size": "OTA paketų failų bendras dydis", + "maximum-ota-package-sum-data-size-required": "OTA paketų failų bendras dydis yra privalomas.", + "maximum-ota-package-sum-data-size-range": "OTA paketų failų bendras dydis negali būti neigiamas", + "maximum-debug-duration-min": "Didžiausia derinimo trukmė (minutėmis)", + "maximum-debug-duration-min-range": "Didžiausia derinimo trukmė negali būti neigiama", + "rest-requests-for-tenant": "REST užklausos nuomininkui", + "transport-tenant-telemetry-msg-rate-limit": "Nuomininko telemetrijos pranešimų limitas", + "transport-tenant-telemetry-data-points-rate-limit": "Nuomininko telemetrijos duomenų taškų limitas", + "transport-device-msg-rate-limit": "Įrenginių pranešimų limitas", + "transport-device-telemetry-msg-rate-limit": "Įrenginių telemetrijos pranešimų limitas", + "transport-device-telemetry-data-points-rate-limit": "Įrenginių telemetrijos duomenų taškų limitas", + "transport-gateway-msg-rate-limit": "Šliuzo pranešimų limitas", + "transport-gateway-telemetry-msg-rate-limit": "Šliuzo telemetrijos pranešimų limitas", + "transport-gateway-telemetry-data-points-rate-limit": "Šliuzo telemetrijos duomenų taškų limitas", + "transport-gateway-device-msg-rate-limit": "Šliuzo įrenginių pranešimų limitas", + "transport-gateway-device-telemetry-msg-rate-limit": "Šliuzo įrenginių telemetrijos pranešimų limitas", + "transport-gateway-device-telemetry-data-points-rate-limit": "Šliuzo įrenginių telemetrijos duomenų taškų limitas", + "tenant-entity-export-rate-limit": "Objektų versijų kūrimo limitas", + "tenant-entity-import-rate-limit": "Objektų versijų įkėlimo limitas", + "tenant-notification-request-rate-limit": "Pranešimų užklausų limitas", + "tenant-notification-requests-per-rule-rate-limit": "Pranešimų užklausų limitas pagal taisyklę", + "max-calculated-fields": "Didžiausias apskaičiuojamų laukų skaičius vienam subjektui", + "max-calculated-fields-range": "Apskaičiuojamų laukų skaičius negali būti neigiamas", + "max-calculated-fields-required": "Didžiausias apskaičiuojamų laukų skaičius vienam subjektui yra privalomas", + "max-data-points-per-rolling-arg": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose", + "max-data-points-per-rolling-arg-range": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose negali būti neigiamas", + "max-data-points-per-rolling-arg-required": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose yra privalomas", + "max-arguments-per-cf": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke", + "max-arguments-per-cf-range": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke negali būti neigiamas", + "max-arguments-per-cf-required": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke yra privalomas", + "max-state-size": "Didžiausias būsenos dydis (KB)", + "max-state-size-range": "Didžiausias būsenos dydis (KB) negali būti neigiamas", + "max-state-size-required": "Didžiausias būsenos dydis (KB) yra privalomas", + "max-value-argument-size": "Didžiausias vieno argumento reikšmės dydis (KB)", + "max-value-argument-size-range": "Didžiausias vieno argumento reikšmės dydis (KB) negali būti neigiamas", + "max-value-argument-size-required": "Didžiausias vieno argumento reikšmės dydis (KB) yra privalomas", + "max-transport-messages": "Didžiausias transporto pranešimų skaičius", + "max-transport-messages-required": "Didžiausias transporto pranešimų skaičius yra privalomas.", + "max-transport-messages-range": "Didžiausias transporto pranešimų skaičius negali būti neigiamas", + "max-transport-data-points": "Didžiausias transporto duomenų taškų skaičius", + "max-transport-data-points-required": "Didžiausias transporto duomenų taškų skaičius yra privalomas.", + "max-transport-data-points-range": "Didžiausias transporto duomenų taškų skaičius negali būti neigiamas", + "max-r-e-executions": "Didžiausias taisyklių variklio vykdymų skaičius", + "max-r-e-executions-required": "Didžiausias taisyklių variklio vykdymų skaičius yra privalomas.", + "max-r-e-executions-range": "Didžiausias taisyklių variklio vykdymų skaičius negali būti neigiamas", + "max-j-s-executions": "Didžiausias JavaScript vykdymų skaičius", + "max-j-s-executions-required": "Didžiausias JavaScript vykdymų skaičius yra privalomas.", + "max-j-s-executions-range": "Didžiausias JavaScript vykdymų skaičius negali būti neigiamas", + "max-tbel-executions": "Didžiausias TBEL vykdymų skaičius", + "max-tbel-executions-required": "Didžiausias TBEL vykdymų skaičius yra privalomas.", + "max-tbel-executions-range": "Didžiausias TBEL vykdymų skaičius negali būti neigiamas", + "max-d-p-storage-days": "Didžiausias duomenų taškų saugojimo dienų skaičius", + "max-d-p-storage-days-required": "Didžiausias duomenų taškų saugojimo dienų skaičius yra privalomas.", + "max-d-p-storage-days-range": "Didžiausias duomenų taškų saugojimo dienų skaičius negali būti neigiamas", + "default-storage-ttl-days": "Numatytasis duomenų saugojimo laikotarpis (TTL dienomis)", + "default-storage-ttl-days-required": "Numatytasis duomenų saugojimo laikotarpis (TTL) yra privalomas.", + "default-storage-ttl-days-range": "Numatytasis duomenų saugojimo laikotarpis (TTL) negali būti neigiamas", + "alarms-ttl-days": "Pavojaus signalų TTL (dienomis)", + "alarms-ttl-days-required": "Pavojaus signalų TTL (dienomis) yra privalomas", + "alarms-ttl-days-days-range": "Pavojaus signalų TTL (dienomis) negali būti neigiamas", + "rpc-ttl-days": "RPC TTL (dienomis)", + "rpc-ttl-days-required": "RPC TTL (dienomis) yra privalomas", + "rpc-ttl-days-days-range": "RPC TTL (dienomis) negali būti neigiamas", + "queue-stats-ttl-days": "Eilių statistikos TTL (dienomis)", + "queue-stats-ttl-days-required": "Eilių statistikos TTL (dienomis) yra privalomas", + "queue-stats-ttl-days-range": "Eilių statistikos TTL (dienomis) negali būti neigiamas", + "rule-engine-exceptions-ttl-days": "Taisyklių variklio išimčių TTL (dienomis)", + "rule-engine-exceptions-ttl-days-required": "Taisyklių variklio išimčių TTL (dienomis) yra privalomas", + "rule-engine-exceptions-ttl-days-range": "Taisyklių variklio išimčių TTL (dienomis) negali būti neigiamas", + "max-rule-node-executions-per-message": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei", + "max-rule-node-executions-per-message-required": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei yra privalomas.", + "max-rule-node-executions-per-message-range": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei negali būti neigiamas", + "max-emails": "Didžiausias išsiųstų el. laiškų skaičius", + "max-emails-required": "Didžiausias išsiųstų el. laiškų skaičius yra privalomas.", + "max-emails-range": "Didžiausias išsiųstų el. laiškų skaičius negali būti neigiamas", + "sms-enabled": "SMS siuntimas įjungtas", + "max-sms": "Didžiausias išsiųstų SMS skaičius", + "max-sms-required": "Didžiausias išsiųstų SMS skaičius yra privalomas.", + "max-sms-range": "Didžiausias išsiųstų SMS skaičius negali būti neigiamas", + "max-created-alarms": "Didžiausias sukurtų pavojaus signalų skaičius", + "max-created-alarms-required": "Didžiausias sukurtų pavojaus signalų skaičius yra privalomas.", + "max-created-alarms-range": "Didžiausias sukurtų pavojaus signalų skaičius negali būti neigiamas", + "no-queue": "Eilė nesukonfigūruota", + "add-queue": "Pridėti eilę", + "queues-with-count": "Eilės ({{count}})", + "tenant-rest-limits": "REST užklausų limitas nuomininkui", + "customer-rest-limits": "REST užklausų limitas klientui", + "incorrect-pattern-for-rate-limits": "Formatas turi būti kableliais atskirtos poros su talpa ir laikotarpiu (sekundėmis), pvz.: 100:1,2000:60", + "too-small-value-zero": "Reikšmė turi būti didesnė nei 0", + "too-small-value-one": "Reikšmė turi būti didesnė nei 1", + "queue-size-is-limited-by-system-configuration": "Eilės dydis taip pat yra ribojamas sistemos konfigūracijos.", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra rašymo užklausos", + "cassandra-read-tenant-core-limits-configuration": "REST API ir WS telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-tenant-rule-engine-limits-configuration": "Taisyklių variklio telemetrijos Cassandra rašymo užklausos", + "cassandra-read-tenant-rule-engine-limits-configuration": "Taisyklių variklio telemetrijos Cassandra skaitymo užklausos", + "ws-limit-max-sessions-per-tenant": "Didžiausias sesijų skaičius vienam nuomininkui", + "ws-limit-max-sessions-per-customer": "Didžiausias sesijų skaičius vienam klientui", + "ws-limit-max-sessions-per-regular-user": "Didžiausias sesijų skaičius vienam vartotojui", + "ws-limit-max-sessions-per-public-user": "Didžiausias sesijų skaičius viešam vartotojui", + "ws-limit-queue-per-session": "Didžiausias pranešimų eilės dydis vienai sesijai", + "ws-limit-max-subscriptions-per-tenant": "Didžiausias prenumeratų skaičius vienam nuomininkui", + "ws-limit-max-subscriptions-per-customer": "Didžiausias prenumeratų skaičius vienam klientui", + "ws-limit-max-subscriptions-per-regular-user": "Didžiausias prenumeratų skaičius vienam vartotojui", + "ws-limit-max-subscriptions-per-public-user": "Didžiausias prenumeratų skaičius viešam vartotojui", + "ws-limit-updates-per-session": "WS atnaujinimų skaičius vienai sesijai", + "rate-limits": { + "add-limit": "Pridėti limitą", + "and-also-less-than": "ir taip pat mažesnis nei", + "advanced-settings": "Pažangūs nustatymai", + "edit-limit": "Redaguoti limitą", + "calculated-field-debug-event-rate-limit": "Apskaičiuojamų laukų derinimo įvykių limitas", + "edit-calculated-field-debug-event-rate-limit": "Redaguoti apskaičiuojamų laukų derinimo įvykių limitus", + "edit-transport-tenant-msg-title": "Redaguoti nuomininko transporto pranešimų limitus", + "edit-transport-tenant-telemetry-msg-title": "Redaguoti nuomininko telemetrijos pranešimų limitus", + "edit-transport-tenant-telemetry-data-points-title": "Redaguoti nuomininko telemetrijos duomenų taškų limitus", + "edit-transport-device-msg-title": "Redaguoti įrenginių transporto pranešimų limitus", + "edit-transport-device-telemetry-msg-title": "Redaguoti įrenginių telemetrijos pranešimų limitus", + "edit-transport-device-telemetry-data-points-title": "Redaguoti įrenginių telemetrijos duomenų taškų limitus", + "edit-transport-gateway-msg-title": "Redaguoti šliuzo pranešimų limitus", + "edit-transport-gateway-telemetry-msg-title": "Redaguoti šliuzo telemetrijos pranešimų limitus", + "edit-transport-gateway-telemetry-data-points-title": "Redaguoti šliuzo telemetrijos duomenų taškų limitus", + "edit-transport-gateway-device-msg-title": "Redaguoti šliuzo įrenginių pranešimų limitus", + "edit-transport-gateway-device-telemetry-msg-title": "Redaguoti šliuzo įrenginių telemetrijos pranešimų limitus", + "edit-transport-gateway-device-telemetry-data-points-title": "Redaguoti šliuzo įrenginių telemetrijos duomenų taškų limitus", + "edit-tenant-rest-limits-title": "Redaguoti REST užklausų limitus nuomininkui", + "edit-customer-rest-limits-title": "Redaguoti REST užklausų limitus klientui", + "edit-ws-limit-updates-per-session-title": "Redaguoti WS atnaujinimų per sesiją limitus", + "edit-cassandra-write-tenant-core-limits-configuration": "Redaguoti REST API Cassandra rašymo užklausų limitus", + "edit-cassandra-read-tenant-core-limits-configuration": "Redaguoti REST API ir WS telemetrijos Cassandra skaitymo užklausų limitus", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Redaguoti taisyklių variklio telemetrijos Cassandra rašymo užklausų limitus", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Redaguoti taisyklių variklio telemetrijos Cassandra skaitymo užklausų limitus", + "edit-tenant-entity-export-rate-limit-title": "Redaguoti objektų versijų kūrimo limitus", + "edit-tenant-entity-import-rate-limit-title": "Redaguoti objektų versijų įkėlimo limitus", + "edit-tenant-notification-request-rate-limit-title": "Redaguoti pranešimų užklausų limitus", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Redaguoti pranešimų užklausų limitus pagal taisyklę", + "edit-edge-events-rate-limit": "Redaguoti Edge įvykių limitus", + "edit-edge-events-per-edge-rate-limit": "Redaguoti Edge įvykių limitus pagal Edge įrenginį", + "edge-events-rate-limit": "Edge įvykiai", + "edge-events-per-edge-rate-limit": "Edge įvykiai pagal Edge įrenginį", + "edit-edge-uplink-messages-rate-limit": "Redaguoti Edge į viršų siunčiamų pranešimų (uplink) limitus", + "edit-edge-uplink-messages-per-edge-rate-limit": "Redaguoti Edge į viršų siunčiamų pranešimų (uplink) limitus pagal Edge įrenginį", + "edge-uplink-messages-rate-limit": "Edge uplink pranešimai", + "edge-uplink-messages-per-edge-rate-limit": "Edge uplink pranešimai pagal Edge įrenginį", + "messages-per": "pranešimų per", + "not-set": "Nenustatyta", + "number-of-messages": "Pranešimų skaičius", + "number-of-messages-required": "Pranešimų skaičius yra privalomas.", + "number-of-messages-min": "Mažiausia reikšmė yra 1.", + "preview": "Peržiūra", + "per-seconds": "Per sekundes", + "per-seconds-required": "Laiko norma yra privaloma.", + "per-seconds-min": "Mažiausia reikšmė yra 1.", + "per-seconds-duplicate": "Pasikartojanti laiko norma. Kiekvienas laiko intervalas turi būti unikalus.", + "rate-limits": "Greitaveikos limitai", + "remove-limit": "Pašalinti limitą", + "transport-tenant-msg": "Nuomininko transporto pranešimai", + "transport-tenant-telemetry-msg": "Nuomininko telemetrijos pranešimai", + "transport-tenant-telemetry-data-points": "Nuomininko telemetrijos duomenų taškai", + "transport-device-msg": "Įrenginių transporto pranešimai", + "transport-device-telemetry-msg": "Įrenginių telemetrijos pranešimai", + "transport-device-telemetry-data-points": "Įrenginių telemetrijos duomenų taškai", + "transport-gateway-msg": "Šliuzo pranešimai", + "transport-gateway-telemetry-msg": "Šliuzo telemetrijos pranešimai", + "transport-gateway-telemetry-data-points": "Šliuzo telemetrijos duomenų taškai", + "transport-gateway-device-msg": "Šliuzo įrenginių pranešimai", + "transport-gateway-device-telemetry-msg": "Šliuzo įrenginių telemetrijos pranešimai", + "transport-gateway-device-telemetry-data-points": "Šliuzo įrenginių telemetrijos duomenų taškai", + "sec": "sek." + } + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, =1 {1 sekundė} other {# sekundės} }", + "minutes-interval": "{ minutes, plural, =1 {1 minutė} other {# minutės} }", + "hours-interval": "{ hours, plural, =1 {1 valanda} other {# valandos} }", + "days-interval": "{ days, plural, =1 {1 diena} other {# dienos} }", + "days": "Dienos", + "hours": "Valandos", + "minutes": "Minutės", + "seconds": "Sekundės", + "advanced": "Išplėstiniai nustatymai", + "custom": "Pasirinktinis", + "predefined": { + "yesterday": "Vakar", + "day-before-yesterday": "Užvakar", + "this-day-last-week": "Ši diena prieš savaitę", + "previous-week": "Praėjusi savaitė (Sek – Šeš)", + "previous-week-iso": "Praėjusi savaitė (Pir – Sek)", + "previous-month": "Praėjęs mėnuo", + "previous-quarter": "Praėjęs ketvirtis", + "previous-half-year": "Praėjęs pusmetis", + "previous-year": "Praėję metai", + "current-hour": "Dabartinė valanda", + "current-day": "Dabartinė diena", + "current-day-so-far": "Dabartinė diena iki dabar", + "current-week": "Dabartinė savaitė (Sek – Šeš)", + "current-week-iso": "Dabartinė savaitė (Pir – Sek)", + "current-week-so-far": "Dabartinė savaitė iki dabar (Sek – Šeš)", + "current-week-iso-so-far": "Dabartinė savaitė iki dabar (Pir – Sek)", + "current-month": "Dabartinis mėnuo", + "current-month-so-far": "Dabartinis mėnuo iki dabar", + "current-quarter": "Dabartinis ketvirtis", + "current-quarter-so-far": "Dabartinis ketvirtis iki dabar", + "current-half-year": "Dabartinis pusmetis", + "current-half-year-so-far": "Dabartinis pusmetis iki dabar", + "current-year": "Dabartiniai metai", + "current-year-so-far": "Dabartiniai metai iki dabar" + }, + "type": { + "week": "Savaitė (Sek – Šeš)", + "week-iso": "Savaitė (Pir – Sek)", + "month": "Mėnuo", + "quarter": "Ketvirtis" + } + }, + "timeunit": { + "milliseconds": "Milisekundės", + "seconds": "Sekundės", + "minutes": "Minutės", + "hours": "Valandos", + "days": "Dienos" + }, + "timewindow": { + "timewindow": "Laiko langas", + "timewindow-settings": "Laiko lango nustatymai", + "years": "{ years, plural, =1 { metai } other {# metai } }", + "years-short": "{{ years }}m", + "months": "{ months, plural, =1 { mėnuo } other {# mėnesiai } }", + "months-short": "{{ months }}mėn", + "weeks": "{ weeks, plural, =1 { savaitė } other {# savaitės } }", + "weeks-short": "{{ weeks }}sav", + "days": "{ days, plural, =1 { diena } other {# dienos } }", + "days-short": "{{ days }}d", + "hours": "{ hours, plural, =0 { valandų } =1 {1 valanda } other {# valandos } }", + "hr": "{{ hr }} val", + "hr-short": "{{ hr }}val", + "minutes": "{ minutes, plural, =0 { minučių } =1 {1 minutė } other {# minutės } }", + "min": "{{ min }} min", + "min-short": "{{ min }}min", + "seconds": "{ seconds, plural, =0 { sekundžių } =1 {1 sekundė } other {# sekundės } }", + "sec": "{{ sec }} sek", + "sec-short": "{{ sec }}s", + "short": { + "years": "{ years, plural, =1 {1 metai } other {# metai } }", + "days": "{ days, plural, =1 {1 diena } other {# dienos } }", + "hours": "{ hours, plural, =1 {1 valanda } other {# valandos } }", + "minutes": "{{ minutes }} min", + "seconds": "{{ seconds }} sek" + }, + "realtime": "Realiu laiku", + "history": "Istorija", + "last-prefix": "Paskutinė", + "period": "nuo {{ startTime }} iki {{ endTime }}", + "edit": "Redaguoti laiko langą", + "date-range": "Datų intervalas", + "for-all-time": "Visas laikotarpis", + "last": "Paskutinė", + "time-period": "Laikotarpis", + "hide": "Slėpti", + "interval": "Intervalas", + "just-now": "Ką tik", + "just-now-lower": "ką tik", + "ago": "prieš", + "style": "Laiko lango stilius", + "icon": "Piktograma", + "icon-position": "Piktogramos padėtis", + "icon-position-left": "Kairėje", + "icon-position-right": "Dešinėje", + "font": "Šriftas", + "color": "Spalva", + "displayTypePrefix": "Rodyti „Realiu laiku“ / „Istorija“ priešdėlį", + "preview": "Peržiūra", + "relative": "Santykinis", + "range": "Intervalas", + "hide-timewindow-section": "Slėpti laiko lango sekciją nuo naudotojų", + "hide-last-interval": "Slėpti paskutinį intervalą nuo naudotojų", + "hide-relative-interval": "Slėpti santykinį intervalą nuo naudotojų", + "hide-fixed-interval": "Slėpti fiksuotą intervalą nuo naudotojų", + "hide-aggregation": "Slėpti agregaciją nuo naudotojų", + "hide-group-interval": "Slėpti grupavimo intervalą nuo naudotojų", + "hide-max-values": "Slėpti maksimalias reikšmes nuo naudotojų", + "hide-timezone": "Slėpti laiko juostą nuo naudotojų", + "disable-custom-interval": "Išjungti pasirinktinių intervalų pasirinkimą", + "edit-aggregation-functions-list": "Redaguoti agregavimo funkcijų sąrašą", + "edit-aggregation-functions-list-hint": "Galima nurodyti galimų funkcijų sąrašą.", + "allowed-aggregation-functions": "Leidžiamos agregavimo funkcijos", + "edit-intervals-list": "Redaguoti intervalų sąrašą", + "allowed-agg-intervals": "Grupavimo intervalai", + "default-agg-interval": "Numatytasis grupavimo intervalas", + "edit-intervals-list-hint": "Galima nurodyti galimų intervalų parinkčių sąrašą.", + "edit-grouping-intervals-list-hint": "Galima sukonfigūruoti grupavimo intervalų sąrašą ir numatytąjį grupavimo intervalą.", + "all": "Visi" + }, + "tooltip": { + "trigger": "Trigeris", + "trigger-point": "Taškas", + "trigger-axis": "Ašis", + "label": "Etiketė", + "value": "Reikšmė", + "date": "Data", + "show-date-time-interval": "Rodyti datos ir laiko intervalą", + "show-date-time-interval-hint": "Rodyti datos ir laiko intervalą pagal duomenų agregaciją.", + "hide-zero-tooltip-values": "Slėpti nulines reikšmes", + "background-color": "Fono spalva", + "background-blur": "Fono suliejimas" + }, + "unit": { + "set-unit-conversion": "Nustatyti matavimo vienetų konvertavimą", + "unit-settings": { + "unit-settings": "Vienetų nustatymai", + "source-unit": "Pirminis vienetas", + "source-unit-hint": "Tai saugomos reikšmės vienetas. Vienetas, iš kurio konvertuojama. Įveskite simbolį, naudojamą jūsų šaltinio duomenyse (pvz., m, km, ft, in).", + "target-metric-unit": "Tikslinis metrinis vienetas", + "target-metric-unit-hint": "Pasirinkite, į kurį metrinį (SI) vienetą konvertuoti šaltinio reikšmę (pvz., cm, mm, km).", + "target-imperial-unit": "Tikslinis imperinis vienetas", + "target-imperial-unit-hint": "Pasirinkite, į kurį imperinį vienetą konvertuoti šaltinio reikšmę (pvz., in, ft, yd).", + "target-hybrid-unit": "Tikslinis mišrus vienetas", + "target-hybrid-unit-hint": "Pasirinkite, į kurį mišrų vienetą konvertuoti šaltinio reikšmę (pvz., cm, in, km). Mišrūs vienetai jungia metrinius ir/arba imperinius vienetus.", + "enable-unit-conversion": "Įjungti vienetų konvertavimą", + "enable-unit-conversion-hint": "Įjunkite, kad suaktyvintumėte konvertavimą. Kai išjungta – šaltinio reikšmė bus perduodama nekeičiama. Išjungta, jei atitinkamoje matavimo grupėje yra tik vienas vienetas (pvz., šviesos srautas, OKI)." + }, + "unit-system": "Matavimo sistema", + "unit-system-type": { + "AUTO": "Automatinė", + "METRIC": "Metrinė", + "IMPERIAL": "Imperinė", + "HYBRID": "Mišri" + }, + "measures": { + "absorbed-dose-rate": "Sugertos dozės greitis", + "acceleration": "Pagreitis", + "acidity": "Rūgštingumas", + "air-quality-index": "Oro kokybės indeksas", + "amount-of-substance": "Medžiagos kiekis", + "angle": "Kampas", + "angular-acceleration": "Kampinis pagreitis", + "area": "Plotas", + "area-density": "Paviršiaus tankis", + "capacitance": "Talpa", + "catalytic-activity": "Katalizinė veikla", + "catalytic-concentration": "Katalizinė koncentracija", + "charge": "Krovinys", + "current-density": "Srovės tankis", + "data-transfer-rate": "Duomenų perdavimo greitis", + "density": "Tankis", + "digital": "Skaitmeninis", + "dimension-ratio": "Matmenų santykis", + "dynamic-viscosity": "Dinaminė klampa", + "earthquake-magnitude": "Žemės drebėjimo stiprumas", + "electric-charge-density": "Elektros krūvio tankis", + "electric-current": "Elektros srovė", + "electric-dipole-moment": "Elektrinis dipolio momentas", + "electric-field-strength": "Elektrinio lauko stipris", + "electric-flux": "Elektrinis srautas", + "electric-permittivity": "Elektrinė skvarba", + "electric-polarizability": "Elektrinė poliariškumas", + "electrical-conductance": "Elektrinė laidumo varža", + "electrical-conductivity": "Elektrinis laidumas", + "energy": "Energija", + "energy-density": "Energijos tankis", + "force": "Jėga", + "frequency": "Dažnis", + "fuel-efficiency": "Kuro sąnaudos / efektyvumas", + "heat-capacity": "Šiluminė talpa", + "illuminance": "Apšvietis", + "inductance": "Induktyvumas", + "kinematic-viscosity": "Kinematinė klampa", + "length": "Ilgis", + "light-exposure": "Šviesos ekspozicija", + "linear-charge-density": "Linijinis krūvio tankis", + "logarithmic-ratio": "Logaritminis santykis", + "luminous-efficacy": "Šviesos efektyvumas", + "luminous-flux": "Šviesos srautas", + "luminous-intensity": "Šviesos stipris", + "magnetic-field-gradient": "Magnetinio lauko gradientas", + "magnetic-flux": "Magnetinis srautas", + "magnetic-flux-density": "Magnetinio srauto tankis", + "magnetic-moment": "Magnetinis momentas", + "magnetic-permeability": "Magnetinė skvarba", + "mass": "Masa", + "mass-fraction": "Masės dalis", + "molar-concentration": "Molinė koncentracija", + "molar-energy": "Molinė energija", + "molar-heat-capacity": "Molinė šiluminė talpa", + "molar-mass": "Molinė masė", + "number-concentration": "Dalelių koncentracija", + "parts-per-million": "Dalinės dalys milijone (ppm)", + "power": "Galia", + "power-density": "Galios tankis", + "pressure": "Slėgis", + "radiance": "Spinduliuotė", + "radiant-intensity": "Spinduliuotės intensyvumas", + "radiation-dose": "Radiacijos dozė", + "radioactive-decay": "Radioaktyvus skilimas", + "radioactivity": "Radioaktyvumas", + "radioactivity-concentration": "Radioaktyvumo koncentracija", + "reciprocal-length": "Atvirkštinis ilgis", + "resistance": "Varža", + "reynolds-number": "Reinoldso skaičius", + "signal-level": "Signalo lygis", + "solid-angle": "Erdvinis kampas", + "specific-energy": "Specifinė energija", + "specific-heat-capacity": "Specifinė šiluminė talpa", + "specific-humidity": "Specifinė drėgmė", + "specific-volume": "Specifinis tūris", + "speed": "Greičio", + "surface-charge-density": "Paviršiaus krūvio tankis", + "surface-tension": "Paviršiaus įtempimas", + "temperature": "Temperatūra", + "thermal-conductivity": "Šilumos laidumas", + "time": "Laikas", + "torque": "Sukimo momentas", + "turbidity": "Drumstumas", + "voltage": "Įtampa", + "volume": "Tūris", + "volume-flow": "Tūrio srautas" + }, + "millimeter": "Milimetras", + "centimeter": "Centimetras", + "decimeter": "Decimetras", + "angstrom": "Angstromas", + "nanometer": "Nanometras", + "micrometer": "Mikrometras", + "meter": "Metras", + "kilometer": "Kilometras", + "inch": "Colis", + "foot": "Pėda", + "foot-us": "Pėda (JAV matavimas)", + "yard": "Jardas", + "mile": "Mylia", + "nautical-mile": "Jūrmylė", + "astronomical-unit": "Astronominis vienetas", + "reciprocal-metre": "Atvirkštinis metras", + "meter-per-meter": "Metrai vienam metrui", + "steradian": "Steradianas", + "thou": "Tausas", + "barleycorn": "Miežio grūdas", + "hand": "Rankos ilgis", + "chain": "Grandinė", + "furlong": "Furlongas", + "league": "Lygos vienetas", + "fathom": "Sazenas", + "cable": "Kabelis", + "link": "Nuoroda", + "rod": "Lazda", + "nanogram": "Nanogramas", + "microgram": "Mikrogramas", + "milligram": "Miligramas", + "gram": "Gramas", + "kilogram": "Kilogramas", + "tonne": "Tona", + "ounce": "Uncija", + "pound": "Svaras", + "stone": "Akmuo (matavimo vienetas)", + "hundredweight-count": "Svorinė šimtinė", + "short-tons": "Trumpa tona", + "dalton": "Daltonas", + "grain": "Grūdas", + "drachm": "Drachma", + "quarter": "Ketvirtis", + "slug": "Slugas", + "carat": "Karat", + "cubic-millimeter": "Kubinis milimetras", + "cubic-centimeter": "Kubinis centimetras", + "cubic-meter": "Kubinis metras", + "cubic-kilometer": "Kubinis kilometras", + "microliter": "Mikrolitras", + "milliliter": "Mililitras", + "liter": "Litras", + "hectoliter": "Hektolitras", + "cubic-inch": "Kubinis colis", + "cubic-foot": "Kubinė pėda", + "cubic-yard": "Kubinis jardas", + "fluid-ounce": "Skysčio uncija", + "fluid-ounce-per-second": "Skysčio uncija per sekundę", + "pint": "Pinta", + "quart": "Kvorta", + "gallon": "Galonas", + "oil-barrels": "Naftos statinė", + "cubic-meter-per-kilogram": "Kubinis metras kilogramui", + "gill": "Gilas", + "hogshead": "Statinė (Hogshead)", + "teaspoon": "Arbatinis šaukštelis", + "tablespoon": "Valgomasis šaukštas", + "cup": "Puodelis", + "celsius": "Celsijus", + "kelvin": "Kelvinas", + "rankine": "Rankinas", + "fahrenheit": "Farenheitas", + "percent": "Procentas", + "meter-per-second": "Metrai per sekundę", + "kilometer-per-hour": "Kilometrai per valandą", + "foot-per-second": "Pėdos per sekundę", + "foot-per-minute": "Pėdos per minutę", + "mile-per-hour": "Mylios per valandą", + "knot": "Mazgas", + "inch-per-second": "Coliai per sekundę", + "inch-per-hour": "Coliai per valandą", + "millimeters-per-minute": "Milimetrai per minutę", + "meter-per-minute": "Metrai per minutę", + "kilometer-per-hour-squared": "Kilometrai per valandą kvadratu", + "foot-per-second-squared": "Pėdos per sekundę kvadratu", + "pascal": "Paskalis", + "kilopascal": "Kilopaskalis", + "megapascal": "Megapaskalis", + "gigapascal": "Gigapaskalis", + "millibar": "Milibaras", + "bar": "Baras", + "kilobar": "Kilobaras", + "newton": "Niutonas", + "newton-meter": "Niutono metras", + "foot-pounds": "Pėdos-svarai", + "inch-pounds": "Colio-svarai", + "newton-per-meter": "Niutonas metrui", + "atmospheres": "Atmosferos", + "pounds-per-square-inch": "Svarai kvadratiniam coliui", + "kilopound-per-square-inch": "Kilosvarai kvadratiniam coliui", + "torr": "Torras", + "inches-of-mercury": "Gyvsidabrio coliai", + "pascal-per-square-meter": "Paskalis kvadratiniam metrui", + "pound-per-square-inch": "Svoris kvadratiniam coliui", + "newton-per-square-meter": "Niutonas kvadratiniam metrui", + "kilogram-force-per-square-meter": "Kilogramo jėga kvadratiniam metrui", + "pascal-per-square-centimeter": "Paskalis kvadratiniam centimetrui", + "ton-force-per-square-inch": "Tonos jėga kvadratiniam coliui", + "kilonewton-per-square-meter": "Kiloniutonas kvadratiniam metrui", + "newton-per-square-millimeter": "Niutonas kvadratiniam milimetrui", + "microjoule": "Mikrodžiaulis", + "millijoule": "Milidžiaulis", + "joule": "Džiaulis", + "kilojoule": "Kilodžiaulis", + "megajoule": "Megadžiaulis", + "gigajoule": "Gigadžiaulis", + "watt-hour": "Vatvalandė", + "watt-minute": "Vatminutė", + "kilowatt-hour": "Kilovatvalandė", + "milliwatt-hour": "Milivatvalandė", + "megawatt-hour": "Megavatvalandė", + "gigawatt-hour": "Gigavatvalandė", + "electron-volts": "Elektronvoltai", + "joules-per-coulomb": "Džiauliai kulonui", + "british-thermal-unit": "Britų šiluminis vienetas (BTU)", + "thousand-british-thermal-unit": "Tūkstantis britų šiluminių vienetų", + "million-british-thermal-unit": "Milijonas britų šiluminių vienetų", + "foot-pound": "Pėdos-svaras", + "calorie": "Kalorija", + "small-calorie": "Mažoji kalorija", + "kilocalorie": "Kilokalorija", + "joule-per-kelvin": "Džiaulis kelvinui", + "joule-per-kilogram-kelvin": "Džiaulis kilogramui-kelvinui", + "joule-per-kilogram": "Džiaulis kilogramui", + "watt-per-meter-kelvin": "Vatas metrui-kelvinui", + "joule-per-cubic-meter": "Džiaulis kubiniam metrui", + "therm": "Termas", + "electric-dipole-moment": "Elektrinis dipolio momentas", + "magnetic-dipole-moment": "Magnetinis dipolio momentas", + "debye": "Debajus", + "coulomb-per-square-meter-per-volt": "Kulonas kvadratiniam metrui voltui", + "milliwatt": "Miliwatas", + "microwatt": "Mikrovatas", + "watt": "Vatas", + "kilowatt": "Kilovatas", + "megawatt": "Megavatas", + "gigawatt": "Gigavatas", + "metric-horsepower": "Metinė arklio galia", + "milliwatt-per-square-centimeter": "Milivatai kvadratiniam centimetrui", + "watt-per-square-centimeter": "Vatai kvadratiniam centimetrui", + "kilowatt-per-square-centimeter": "Kilovatai kvadratiniam centimetrui", + "milliwatt-per-square-meter": "Milivatai kvadratiniam metrui", + "watt-per-square-meter": "Vatai kvadratiniam metrui", + "kilowatt-per-square-meter": "Kilovatai kvadratiniam metrui", + "watt-per-square-inch": "Vatai kvadratiniam coliui", + "kilowatt-per-square-inch": "Kilovatai kvadratiniam coliui", + "horsepower": "Arklio galia", + "btu-per-hour": "Britų šiluminiai vienetai per valandą", + "btu-per-second": "Britų šiluminiai vienetai per sekundę", + "btu-per-day": "Britų šiluminiai vienetai per dieną", + "mbtu-per-hour": "Tūkstantis britų šiluminių vienetų per valandą", + "mbtu-per-second": "Tūkstantis britų šiluminių vienetų per sekundę", + "mbtu-per-day": "Tūkstantis britų šiluminių vienetų per dieną", + "mmbtu-per-hour": "Milijonas britų šiluminių vienetų per valandą", + "mmbtu-per-second": "Milijonas britų šiluminių vienetų per sekundę", + "mmbtu-per-day": "Milijonas britų šiluminių vienetų per dieną", + "foot-pound-per-second": "Pėdos-svarai per sekundę", + "coulomb": "Kulonas", + "millicoulomb": "Milikulonas", + "microcoulomb": "Mikrokulonas", + "nanocoulomb": "Nanokulonas", + "picocoulomb": "Pikokulonas", + "coulomb-per-meter": "Kulonai metrui", + "coulomb-per-cubic-meter": "Kulonai kubiniam metrui", + "coulomb-per-square-meter": "Kulonai kvadratiniam metrui", + "square-millimeter": "Kvadratinis milimetras", + "square-centimeter": "Kvadratinis centimetras", + "square-meter": "Kvadratinis metras", + "hectare": "Hektaras", + "square-kilometer": "Kvadratinis kilometras", + "square-inch": "Kvadratinis colis", + "square-foot": "Kvadratinė pėda", + "square-yard": "Kvadratinis jardas", + "acre": "Akras", + "square-mile": "Kvadratinė mylia", + "are": "Aras", + "barn": "Barnas", + "circular-inch": "Apskritiminis colis", + "milliampere-hour": "Miliampervalandė", + "ampere-hours": "Ampervalandės", + "kiloampere-hours": "Kiloampervalandės", + "nanoampere": "Nanoamperas", + "picoampere": "Pikoamperas", + "microampere": "Mikroamperas", + "milliampere": "Miliamperas", + "ampere": "Amperas", + "kiloampere": "Kiloamperas", + "megaampere": "Megaamperas", + "gigaampere": "Gigaamperas", + "microampere-per-square-centimeter": "Mikroamperai kvadratiniam centimetrui", + "ampere-per-square-meter": "Amperai kvadratiniam metrui", + "ampere-per-meter": "Amperai metrui", + "oersted": "Erstedas", + "bohr-magneton": "Boro magnetonas", + "ampere-meter-squared": "Ampermetrai kvadratu", + "nanovolt": "Nanovoltas", + "picovolt": "Pikovoltas", + "millivolt": "Milivoltas", + "microvolt": "Mikrovoltas", + "volt": "Voltas", + "kilovolt": "Kilovoltas", + "megavolt": "Megavoltas", + "dbmV": "dBmV", + "dbm": "Decibelmilivatai", + "volt-meter": "Voltmetras", + "kilovolt-meter": "Kilovoltmetras", + "megavolt-meter": "Megavoltmetras", + "microvolt-meter": "Mikrovoltmetras", + "millivolt-meter": "Milivoltmetras", + "nanovolt-meter": "Nanovoltmetras", + "ohm": "Omas", + "microohm": "Mikroomas", + "milliohm": "Milioomas", + "kilohm": "Kiloomas", + "megohm": "Megaomas", + "gigohm": "Gigaomas", + "millihertz": "Milihercas", + "hertz": "Hercas", + "kilohertz": "Kilohercas", + "megahertz": "Megahercas", + "gigahertz": "Gigahercas", + "terahertz": "Terahercas", + "rpm": "Apsisukimai per minutę", + "candela-per-square-meter": "Kandela kvadratiniam metrui", + "candela": "Kandela", + "lumen": "Liumenas", + "lux": "Liuksas", + "foot-candle": "Pėdos žvakė", + "lumen-per-square-meter": "Liumenai kvadratiniam metrui", + "lux-second": "Liuksas sekundei", + "lumen-second": "Liumenas sekundei", + "lumens-per-watt": "Liumenai vatui", + "mole": "Molis", + "nanomole": "Nanomolis", + "micromole": "Mikromolis", + "millimole": "Milimolis", + "kilomole": "Kilomolis", + "mole-per-cubic-meter": "Moliai kubiniam metrui", + "rssi": "RSSI (signalo stiprumas)", + "ppm": "Dalis milijone (ppm)", + "ppb": "Dalis milijarde (ppb)", + "micrograms-per-cubic-meter": "Mikrogramai kubiniam metrui", + "aqi": "Oro kokybės indeksas (OKI)", + "gram-per-cubic-meter": "Gramai kubiniam metrui", + "gram-per-kilogram": "Specifinis drėgnis (g/kg)", + "millimeters-per-second": "Milimetrai per sekundę", + "neper": "Neperas", + "bel": "Belas", + "decibel": "Decibelas", + "meters-per-second-squared": "Metrai per sekundę kvadratu", + "becquerel": "Bekerelis", + "curie": "Kiuris", + "gray": "Grėjus", + "sievert": "Zyvertas", + "roentgen": "Rentgenas", + "cps": "Impulsai per sekundę", + "rad": "Radas", + "rem": "Remas", + "dps": "Skaidymasis per sekundę", + "rutherford": "Ruterfordas", + "coulombs-per-kilogram": "Kulonai kilogramui", + "becquerels-per-cubic-meter": "Bekereliai kubiniam metrui", + "curies-per-liter": "Kiuriai litrui", + "becquerels-per-second": "Bekereliai per sekundę", + "curies-per-second": "Kiuriai per sekundę", + "gy-per-second": "Grėjai per sekundę", + "watt-per-steradian": "Vatai steradianui", + "watt-per-square-metre-steradian": "Vatai kvadratiniam metrui-steradianui", + "ph-level": "pH lygis", + "turbidity": "Drumstumas", + "mg-per-liter": "Miligramai litrui", + "microsiemens-per-centimeter": "Mikrosiemensai centimetrui", + "millisiemens-per-meter": "Milisiemensai metrui", + "siemens-per-meter": "Siemensai metrui", + "kilogram-per-cubic-meter": "Kilogramai kubiniam metrui", + "gram-per-cubic-centimeter": "Gramai kubiniam centimetrui", + "kilogram-per-square-meter": "Kilogramai kvadratiniam metrui", + "milligram-per-milliliter": "Miligramai mililitrui", + "milligram-per-cubic-meter": "Miligramai kubiniam metrui", + "pound-per-cubic-foot": "Svarai kubinei pėdai", + "ounces-per-cubic-inch": "Uncijos kubiniam coliui", + "tons-per-cubic-yard": "Tonos kubiniam jardui", + "particle-density": "Dalinė tankio vertė", + "kilometers-per-liter": "Kilometrai litrui", + "miles-per-gallon": "Mylios galonui", + "liters-per-100-km": "Litrai 100 kilometrų", + "gallons-per-mile": "Galonai myliai", + "liters-per-hour": "Litrai per valandą", + "gallons-per-hour": "Galonai per valandą", + "beats-per-minute": "Dūžiai per minutę", + "millimeters-of-mercury": "Gyvsidabrio milimetrai", + "milligrams-per-deciliter": "Miligramai decilitrui", + "g-force": "G-jėga", + "kilonewton": "Kiloniutonas", + "kilogram-force": "Kilogramo jėga", + "pound-force": "Svarinė jėga", + "kilopound-force": "Kilosvarinė jėga", + "dyne": "Dina", + "poundal": "Paundalas", + "kip": "Kipas", + "gal": "Galas", + "gravity": "Gravitacija", + "hectopascal": "Hektopaskalis", + "atmosphere": "Atmosfera", + "millibars": "Milibarai", + "inch-of-mercury": "Gyvsidabrio colis", + "richter-scale": "Richtėro skalė", + "nanosecond": "Nanosekundė", + "microsecond": "Mikrosekundė", + "millisecond": "Milisekundė", + "second": "Sekundė", + "minute": "Minutė", + "hour": "Valanda", + "day": "Diena", + "week": "Savaitė", + "month": "Mėnuo", + "year": "Metai", + "cubic-foot-per-minute": "Kubinės pėdos per minutę", + "cubic-meters-per-hour": "Kubiniai metrai per valandą", + "cubic-meters-per-second": "Kubiniai metrai per sekundę", + "liter-per-second": "Litrai per sekundę", + "liter-per-minute": "Litrai per minutę", + "gallons-per-minute": "Galonai per minutę", + "cubic-foot-per-second": "Kubinė pėda per sekundę", + "milliliters-per-minute": "Mililitrai per minutę", + "cubic-decimeter-per-second": "Kubinis decimetras per sekundę", + "bit": "Bitas", + "byte": "Baitas", + "kilobyte": "Kilobaitas", + "megabyte": "Megabaitas", + "gigabyte": "Gigabaitas", + "terabyte": "Terabaitas", + "petabyte": "Petabaitas", + "exabyte": "Eksabaitas", + "zettabyte": "Zetabaitas", + "yottabyte": "Jotabaitas", + "bit-per-second": "Bitai per sekundę", + "kilobit-per-second": "Kilobitai per sekundę", + "megabit-per-second": "Megabitai per sekundę", + "gigabit-per-second": "Gigabitai per sekundę", + "terabit-per-second": "Terabitai per sekundę", + "byte-per-second": "Baitai per sekundę", + "kilobyte-per-second": "Kilobaitai per sekundę", + "megabyte-per-second": "Megabaitai per sekundę", + "gigabyte-per-second": "Gigabaitai per sekundę", + "degree": "Laipsnis", + "radian": "Radianas", + "gradian": "Gradianas", + "arcminute": "Kampinė minutė", + "arcsecond": "Kampinė sekundė", + "milliradian": "Miliradianas", + "revolution": "Apsisukimas", + "siemens": "Siemensas", + "millisiemens": "Milisiemensas", + "microsiemens": "Mikrosiemensas", + "kilosiemens": "Kilosiemensas", + "megasiemens": "Megasiemensas", + "gigasiemens": "Gigasiemensas", + "farad": "Faradas", + "millifarad": "Milifaradas", + "microfarad": "Mikrofaradas", + "nanofarad": "Nanofaradas", + "picofarad": "Pikofaradas", + "kilofarad": "Kilofaradas", + "megafarad": "Megafaradas", + "gigafarad": "Gigafaradas", + "terfarad": "Terafaradas", + "farad-per-meter": "Faradai metrui", + "tesla": "Tesla", + "gauss": "Gausas", + "kilogauss": "Kilogausas", + "millitesla": "Militesla", + "microtesla": "Mikrotesla", + "nanotesla": "Nanotesla", + "kilotesla": "Kilotesla", + "megatesla": "Megatesla", + "millitesla-square-meters": "Militeslos kvadratiniai metrai", + "gamma": "Gama", + "lambda": "Lambda", + "square-meter-per-second": "Kvadratiniai metrai per sekundę", + "square-centimeter-per-second": "Kvadratiniai centimetrai per sekundę", + "stoke": "Stokas", + "centistokes": "Centistokai", + "square-foot-per-second": "Kvadratinės pėdos per sekundę", + "square-inch-per-second": "Kvadratiniai coliai per sekundę", + "pascal-second": "Paskalų sekundė", + "centipoise": "CentiPoisas", + "poise": "Poisas", + "reynolds": "Reinoldso skaičius", + "pound-per-foot-hour": "Svarai pėdai per valandą", + "newton-second-per-square-meter": "Niutono sekundė kvadratiniam metrui", + "dyne-second-per-square-centimeter": "Dinos sekundė kvadratiniam centimetrui", + "kilogram-per-meter-second": "Kilogramai metrui per sekundę", + "tesla-square-meters": "Teslos kvadratiniai metrai", + "maxwell": "Maksvelas", + "tesla-per-meter": "Teslos metrui", + "gauss-per-centimeter": "Gausai centimetrui", + "weber": "Veberis", + "microweber": "Mikroveberis", + "milliweber": "Miliveberis", + "gauss-square-centimeter": "Gausai kvadratiniam centimetrui", + "kilogauss-square-centimeter": "Kilogausai kvadratiniam centimetrui", + "henry": "Henris", + "millihenry": "Milihenris", + "microhenry": "Mikrohenris", + "nanohenry": "Nanohenris", + "henry-per-meter": "Henriai metrui", + "tesla-meter-per-ampere": "Teslos metras amperui", + "gauss-per-oersted": "Gausai erstedui", + "kilogram-per-mole": "Kilogramai moliui", + "gram-per-mole": "Gramai moliui", + "milligram-per-mole": "Miligramai moliui", + "joule-per-mole": "Džiauliai moliui", + "joule-per-mole-kelvin": "Džiauliai moliui-kelvinui", + "millivolts-per-meter": "Milivoltai metrui", + "volts-per-meter": "Voltai metrui", + "kilovolts-per-meter": "Kilovoltai metrui", + "radian-per-second": "Radianai per sekundę", + "radian-per-second-squared": "Radianai per sekundę kvadratu", + "revolutions-per-minute-per-second": "Apsisukimai per minutę per sekundę", + "deg-per-second": "laipsn./s", + "rotation-per-minute": "Apsisukimai per minutę", + "degrees-brix": "Brix laipsniai", + "katal": "Katalis", + "katal-per-cubic-metre": "Kataliai kubiniam metrui", + "paris-inch": "Paryžiaus colis" + }, + "user": { + "user": "Vartotojas", + "users": "Vartotojai", + "customer-users": "Kliento vartotojai", + "tenant-admins": "Valdytojų administratoriai", + "sys-admin": "Sistemos administratoriai", + "tenant-admin": "Valdytojo administratoriai", + "customer": "Klientas", + "anonymous": "Anonimas", + "add": "Pridėti vartotoją", + "delete": "Panaikinti vartotoją", + "add-user-text": "Pridėti naują vartotoją", + "no-users-text": "Vartotojų nėra", + "user-details": "Informacija apie vartotoją", + "delete-user-title": "Ar tikrai norite panaikinti vartotoją '{{userEmail}}'?", + "delete-user-text": "Būkite dėmesingi – po patvirtinimo vartotojas ir visa su juo susijusi informacija bus panaikinta.", + "delete-users-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }?", + "delete-users-action-title": "Panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }", + "delete-users-text": "Būkite dėmesingi – po patvirtinimo visi pasirinkti vartotojai ir su jais susijusi informacija bus panaikinta.", + "activation-email-sent-message": "Aktyvinimo el. laiškas sėkmingai išsiųstas!", + "resend-activation": "Iš naujo siųsti aktyvavimo laišką", + "email": "El. pašto adresas", + "email-required": "El. pašto adresas būtinas.", + "invalid-email-format": "Neteisingas el. pašto adreso formatas.", + "first-name": "Vardas", + "last-name": "Pavardė", + "description": "Aprašymas", + "default-dashboard": "Pagrindinis skydelis", + "always-fullscreen": "Visada per visą ekraną", + "select-user": "Pasirinkti vartotoją", + "no-users-matching": "Vartotojų, atitinkančių '{{entity}}', nėra.", + "user-required": "Vartotojas būtinas", + "activation-method": "Aktyvavimo būdas", + "display-activation-link": "Rodyti aktyvavimo nuorodą", + "send-activation-mail": "Siųsti aktyvavimo el. laišką", + "activation-link": "Vartotojo aktyvavimo nuoroda", + "activation-link-text": "Norėdami aktyvuoti vartotoją, paspauskite nuorodą:", + "copy-activation-link": "Kopijuoti aktyvavimo nuorodą", + "activation-link-copied-message": "Vartotojo aktyvavimo nuoroda nukopijuota į iškarpinę", + "details": "Informacija", + "login-as-tenant-admin": "Prisijungti kaip valdytojo administratorius", + "login-as-customer-user": "Prisijungti kaip kliento vartotojas", + "search": "Vartotojų paieška", + "selected-users": "Pasirinkta { count, plural, =1 {1 vartotojas} other {# vartotojai} }", + "disable-account": "Išjungti vartotojo paskyrą", + "enable-account": "Įjungti vartotojo paskyrą", + "enable-account-message": "Vartotojo paskyra sėkmingai įjungta!", + "disable-account-message": "Vartotojo paskyra sėkmingai išjungta!", + "copyId": "Kopijuoti vartotojo ID", + "idCopiedMessage": "Vartotojo ID nukopijuotas į iškarpinę", + "user-list": "Vartotojų sąrašas", + "user-list-required": "Vartotojų sąrašas būtinas" + }, + "value": { + "type": "Reikšmės tipas", + "string": "Tekstas", + "string-value": "Tekstinė informacija", + "string-value-required": "Tekstinė informacija būtina", + "integer": "Sveikasis skaičius", + "integer-value": "Sveikojo skaičiaus reikšmė", + "integer-value-required": "Sveikojo skaičiaus reikšmė būtina", + "invalid-integer-value": "Sveikojo skaičiaus reikšmė neteisinga", + "double": "Realusis skaičius", + "double-value": "Realiojo skaičiaus reikšmė", + "double-value-required": "Realiojo skaičiaus reikšmė būtina", + "boolean": "Loginis", + "boolean-value": "Loginė reikšmė", + "false": "Netiesa", + "true": "Tiesa", + "long": "Sveikas skaičius", + "json": "JSON", + "json-value": "JSON reikšmė", + "json-value-invalid": "Neteisingas JSON reikšmės formatas", + "json-value-required": "JSON reikšmė būtina." + }, + "version-control": { + "version-control": "Versijų valdymas", + "management": "Versijų valdymo administravimas", + "search": "Ieškoti versijų", + "branch": "Šaka", + "default": "Numatytoji", + "select-branch": "Pasirinkite šaką", + "branch-required": "Šaka būtina", + "create-entity-version": "Sukurti subjekto versiją", + "version-name": "Versijos pavadinimas", + "version-name-required": "Versijos pavadinimas būtinas", + "author": "Autorius", + "export-relations": "Eksportuoti ryšius", + "export-attributes": "Eksportuoti atributus", + "export-credentials": "Eksportuoti įgaliojimus", + "export-calculated-fields": "Eksportuoti skaičiuojamus laukus", + "entity-versions": "Subjektų versijos", + "versions": "Versijos", + "created-time": "Sukūrimo laikas", + "version-id": "Versijos ID", + "no-entity-versions-text": "Subjektų versijų nerasta", + "no-versions-text": "Versijų nerasta", + "copy-full-version-id": "Kopijuoti pilną versijos ID", + "create-version": "Sukurti versiją", + "creating-version": "Kuriama versija... Prašome palaukti", + "nothing-to-commit": "Nėra pakeitimų, kuriuos galima įkelti", + "restore-version": "Atkurti versiją", + "restore-entity-from-version": "Atkurti subjektą iš versijos '{{versionName}}'", + "restoring-entity-version": "Atkuriama subjekto versija... Prašome palaukti", + "load-relations": "Įkelti ryšius", + "load-attributes": "Įkelti atributus", + "load-credentials": "Įkelti įgaliojimus", + "load-calculated-fields": "Įkelti skaičiuojamus laukus", + "compare-with-current": "Palyginti su dabartine", + "diff-entity-with-version": "Palyginti su subjekto versija '{{versionName}}'", + "previous-difference": "Ankstesnis skirtumas", + "next-difference": "Kitas skirtumas", + "current": "Dabartinė", + "differences": "{ count, plural, =1 {1 skirtumas} other {# skirtumai} }", + "create-entities-version": "Sukurti subjektų versiją", + "default-sync-strategy": "Numatytoji sinchronizavimo strategija", + "sync-strategy-merge": "Sujungti (Merge)", + "sync-strategy-overwrite": "Perrašyti (Overwrite)", + "entities-to-export": "Subjektai eksportui", + "entities-to-restore": "Subjektai atkūrimui", + "sync-strategy": "Sinchronizavimo strategija", + "all-entities": "Visi subjektai", + "no-entities-to-export-prompt": "Nurodykite subjektus, kuriuos norite eksportuoti", + "no-entities-to-restore-prompt": "Nurodykite subjektus, kuriuos norite atkurti", + "add-entity-type": "Pridėti subjekto tipą", + "remove-all": "Pašalinti visus", + "version-create-result": "{ added, plural, =0 {Subjektų nepridėta} =1 {1 subjektas pridėtas} other {# subjektai pridėti} }.
    { modified, plural, =0 {Neatnaujinta nė vieno subjekto} =1 {1 subjektas atnaujintas} other {# subjektai atnaujinti} }.
    { removed, plural, =0 {Neištrinta nė vieno subjekto} =1 {1 subjektas pašalintas} other {# subjektai pašalinti} }.", + "remove-other-entities": "Pašalinti kitus subjektus", + "find-existing-entity-by-name": "Rasti esamą subjektą pagal pavadinimą", + "restore-entities-from-version": "Atkurti subjektus iš versijos '{{versionName}}'", + "restoring-entities-from-version": "Atkuriami subjektai... Prašome palaukti", + "no-entities-restored": "Neatkurtas nė vienas subjektas", + "created": "{{created}} sukurta", + "updated": "{{updated}} atnaujinta", + "deleted": "{{deleted}} ištrinta", + "remove-other-entities-confirm-text": "Būkite atsargūs! Tai negrįžtamai ištrins visus dabartinius subjektus,
    kurių nėra atkuriamoje versijoje.

    Įveskite „remove other entities“, kad patvirtintumėte.", + "auto-commit-to-branch": "automatiškai įkelti į šaką {{ branch }}", + "default-create-entity-version-name": "{{entityName}} atnaujinimas", + "sync-strategy-merge-hint": "Sukuria arba atnaujina pasirinktus subjektus saugykloje. Kiti saugyklos subjektai neliečiami.", + "sync-strategy-overwrite-hint": "Sukuria arba atnaujina pasirinktus subjektus saugykloje. Visi kiti saugyklos subjektai bus ištrinti.", + "device-credentials-conflict": "Nepavyko įkelti įrenginio su išoriniu ID {{entityId}}
    nes tie patys įgaliojimai jau egzistuoja kitam įrenginiui duomenų bazėje.
    Apsvarstykite galimybę išjungti įkelti įgaliojimus nustatymą atkūrimo formoje.", + "missing-referenced-entity": "Nepavyko įkelti {{sourceEntityTypeName}} su išoriniu ID {{sourceEntityId}}
    nes jis nurodo neegzistuojantį {{targetEntityTypeName}} su ID {{targetEntityId}}.", + "runtime-failed": "Klaida: {{message}}", + "auto-commit-settings-read-only-hint": "Automatinio įkėlimo funkcija neveikia, kai saugykla nustatyta tik skaitymui.", + "rollback-on-error": "Atšaukti pakeitimus klaidos atveju", + "rollback-on-error-hint": "Jei atkuriate didelį subjektų kiekį, galite išjungti šią parinktį našumui padidinti.\nAtminkite: jei atkūrimo metu įvyks klaida, jau išsaugoti subjektai (su ryšiais, atributais ir kt.) liks nepakitę." + }, + "widget": { + "widget-library": "Valdiklių galerija", + "widget-bundle": "Valdiklių rinkinys", + "all-bundles": "Visi rinkiniai", + "select-widgets-bundle": "Pasirinkti valdiklių rinkinį", + "widgets": "Valdikliai", + "all-widgets": "Visi valdikliai", + "widget": "Valdiklis", + "select-widget": "Pasirinkti valdiklį", + "no-widgets-matching": "Valdiklių atitinkančių '{{entity}}' nėra.", + "no-widgets": "Valdiklių dar nėra", + "no-widgets-text": "Valdiklių nėra", + "management": "Valdiklių valdymas", + "editor": "Valdiklių redaktorius", + "confirm-to-exit-editor-html": "Turite neišsaugotų valdiklio nustatymų.
    Ar tikrai norite palikti šį puslapį?", + "widget-type-not-found": "Įkeliant valdiklio konfigūraciją įvyko klaida.
    Gali būti, kad šis valdiklio tipas buvo pašalintas.", + "widget-type-load-error": "Valdiklis neįkeltas dėl klaidų:", + "remove": "Panaikinti valdiklį", + "delete": "Panaikinti valdiklį", + "edit": "Redaguoti valdiklį", + "remove-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTitle}}'?", + "remove-widget-text": "Būkite dėmesingi — po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", + "replace-reference-with-widget-copy": "Pakeisti nuorodą valdiklio kopija", + "timeseries": "Telemetrija", + "search-data": "Duomenų paieška", + "no-data-found": "Duomenų nerasta", + "latest": "Naujausios reikšmės", + "rpc": "Valdymo valdiklis", + "alarm": "Įspėjimų valdiklis", + "static": "Statinis valdiklis", + "timeseries-short": "telemetrija", + "latest-short": "naujausios", + "rpc-short": "valdymas", + "alarm-short": "įspėjimas", + "static-short": "statinis", + "select-widget-type": "Pasirinkite valdiklio tipą", + "missing-widget-title-error": "Valdiklio pavadinimas būtinas!", + "widget-saved": "Valdiklis išsaugotas", + "unable-to-save-widget-error": "Valdiklio išsaugoti nepavyko! Yra klaidų konfigūracijoje.", + "save": "Išsaugoti valdiklį", + "saveAs": "Išsaugoti valdiklį kaip", + "move": "Perkelti valdiklį", + "save-widget-as": "Išsaugoti valdiklį kaip", + "save-widget-as-text": "Įveskite naują valdiklio pavadinimą", + "toggle-fullscreen": "Perjungti į viso ekrano režimą", + "run": "Paleisti valdiklį", + "widget-title": "Valdiklio pavadinimas", + "title": "Pavadinimas", + "title-required": "Valdiklio pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "system": "Sisteminis", + "type": "Tipas", + "resources": "Resursai", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "Yra plėtinys", + "remove-resource": "Pašalinti resursą", + "add-resource": "Pridėti resursą", + "html": "HTML", + "tidy": "Tvarkyti (Tidy)", + "css": "CSS", + "settings-form": "Nustatymų forma", + "data-key-settings-form": "Duomenų rakto nustatymų forma", + "latest-data-key-settings-form": "Naujausių duomenų rakto nustatymų forma", + "widget-settings": "Valdiklio nustatymai", + "description": "Aprašymas", + "tags": "Žymos", + "image-preview": "Paveiksliuko peržiūra", + "settings-form-selector": "Nustatymų formos pasirinkiklis", + "data-key-settings-form-selector": "Duomenų rakto nustatymų formos pasirinkiklis", + "latest-data-key-settings-form-selector": "Naujausių duomenų rakto nustatymų formos pasirinkiklis", + "all": "Visi", + "actual": "Esamas", + "scada": "SCADA simbolis", + "deprecated": "Pasenęs", + "has-basic-mode": "Turi bazinį režimą", + "basic-mode-form-selector": "Bazinio režimo formos pasirinkiklis", + "basic-mode": "Bazinė versija", + "advanced-mode": "Pažangi versija", + "javascript": "JavaScript", + "js": "JS", + "delete-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetName}}'?", + "delete-widget-text": "Po patvirtinimo valdiklis ir visa susijusi informacija bus negrįžtamai pašalinti.", + "delete-widgets-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklį} other {# valdiklius} }?", + "delete-widgets-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti valdikliai ir susijusi informacija bus negrįžtamai pašalinta.", + "delete-widget": "Panaikinti valdiklį", + "widget-template-load-failed-error": "Nepavyko įkelti valdiklio šablono!", + "details": "Informacija", + "widget-details": "Informacija apie valdiklį", + "add": "Pridėti valdiklį", + "add-existing-widget": "Pridėti esamą valdiklį", + "add-new-widget": "Pridėti naują valdiklį", + "search-widgets": "Valdiklių paieška", + "selected-widgets": "Pasirinkta { count, plural, =1 {1 valdiklis} other {# valdikliai} }", + "undo": "Atšaukti valdiklio pakeitimus", + "export": "Eksportuoti valdiklį", + "export-prompt": "Įtraukti valdiklio paveikslėlius ir resursus", + "export-widgets": "Eksportuoti valdiklius", + "export-widgets-prompt": "Įtraukti valdiklių paveikslėlius ir resursus", + "import": "Importuoti valdiklį", + "no-data": "Nėra duomenų atvaizdavimui", + "data-overflow": "Valdiklyje rodoma {{count}} iš {{total}} įrašų", + "alarm-data-overflow": "Valdiklyje rodomi {{allowedEntities}} (maksimalus leistinas skaičius) įspėjimų įrašai iš {{totalEntities}}", + "search": "Valdiklių paieška", + "filter": "Valdiklių filtro tipas", + "loading-widgets": "Įkeliami valdikliai...", + "widget-template-error": "Netinkamas valdiklio HTML šablonas.", + "reference": "Nuoroda" + }, + "widget-action": { + "header-button": "Valdiklio antraštės mygtukas", + "do-nothing": "Nedaryti nieko", + "open-dashboard-state": "Eiti į naują skydelio būseną", + "update-dashboard-state": "Atnaujinti dabartinę skydelio būseną", + "open-dashboard": "Eiti į kitą skydelį", + "custom": "Aprašomas veiksmas", + "custom-pretty": "Aprašomas veiksmas (su HTML šablonu)", + "custom-pretty-error-title": "Klaida vykdant pasirinktą dialogą", + "custom-pretty-template-error": "Netinkamas pasirinktinio dialogo šablonas.", + "custom-pretty-controller-error": "Įvyko klaida vykdant pasirinktinio dialogo funkciją.", + "mobile-action": "Mobilusis veiksmas", + "target-dashboard-state": "Tikslinė skydelio būsena", + "target-dashboard-state-required": "Tikslnė skydelio būsena būtina", + "set-entity-from-widget": "Nustatyti subjektą iš valdiklio", + "target-dashboard": "Tikslinis skydelis", + "select-target-dashboard": "Pasirinkite tikslinį skydelį", + "target-dashboard-required": "Tikslinis skydelis būtinas.", + "open-right-layout": "Atidaryti tinkamą skydelio išdėstymą (mobilusis vaizdas)", + "state-display-type": "Skydelio būsenos rodymo būdas", + "open-normal": "Normalus vaizdas", + "open-in-separate-dialog": "Atidaryti atskirame dialoge", + "open-in-popover": "Atidaryti iškylančiame lange", + "dialog-title": "Dialogo pavadinimas", + "dialog-hide-dashboard-toolbar": "Slėpti skydelio įrankių juostą dialoge", + "dialog-width": "Dialogo plotis (procentais nuo lango pločio)", + "dialog-height": "Dialogo aukštis (procentais nuo lango aukščio)", + "dialog-size-range-error": "Dialogo dydžio reikšmė turi būti nuo 1 iki 100 procentų.", + "popover-preferred-placement": "Pageidaujama iškylančio lango padėtis", + "popover-placement-top": "Viršuje", + "popover-placement-topLeft": "Viršuje kairėje", + "popover-placement-topRight": "Viršuje dešinėje", + "popover-placement-right": "Dešinėje", + "popover-placement-rightTop": "Dešinėje viršuje", + "popover-placement-rightBottom": "Dešinėje apačioje", + "popover-placement-bottom": "Apačioje", + "popover-placement-bottomLeft": "Apačioje kairėje", + "popover-placement-bottomRight": "Apačioje dešinėje", + "popover-placement-left": "Kairėje", + "popover-placement-leftTop": "Kairėje viršuje", + "popover-placement-leftBottom": "Kairėje apačioje", + "popover-hide-on-click-outside": "Slėpti iškylantį langą spustelėjus už ribų", + "popover-hide-dashboard-toolbar": "Slėpti skydelio įrankių juostą iškylančiame lange", + "popover-width": "Iškylančio lango plotis naršyklės vienetais (pvz. 100px, 25vw)", + "popover-height": "Iškylančio lango aukštis naršyklės vienetais (pvz. 100px, 25vh)", + "popover-style": "Iškylančio lango stilius", + "open-new-browser-tab": "Atidaryti naujame naršyklės skirtuke", + "open-URL": "Atidaryti URL", + "URL": "URL", + "url-required": "URL būtinas.", + "mobile": { + "device-provision": "Įrenginio paruošimas", + "action-type": "Mobiliojo veiksmo tipas", + "select-action-type": "Pasirinkite mobiliojo veiksmo tipą", + "action-type-required": "Mobiliojo veiksmo tipas būtinas.", + "take-picture-from-gallery": "Pasirinkti nuotrauką iš galerijos", + "take-photo": "Padaryti nuotrauką", + "map-direction": "Atidaryti maršrutą žemėlapyje", + "map-location": "Atidaryti vietą žemėlapyje", + "scan-qr-code": "Skenuoti QR kodą", + "make-phone-call": "Skambinti telefonu", + "get-location": "Gauti telefono vietą", + "take-screenshot": "Padaryti ekrano nuotrauką" + }, + "custom-action-function": "Pasirinktinio veiksmo funkcija", + "custom-pretty-function": "Pasirinktinio veiksmo (su HTML šablonu) funkcija", + "map-item-type": "Žemėlapio elemento tipas", + "map-item": { + "marker": "Žymeklis", + "polygon": "Daugiakampis", + "rectangle": "Stačiakampis", + "circle": "Apskritimas" + }, + "place-map-item": "Įdėti žemėlapio elementą", + "map-item-tooltip": { + "customize-map-item-tooltips": "Tinkinti žemėlapio elementų paaiškinimus (tooltips)", + "place-marker": "Padėti žymeklį", + "start-draw-rectangle": "Pradėti piešti stačiakampį", + "finish-draw-rectangle": "Baigti piešti stačiakampį", + "start-draw-polygon": "Pradėti piešti daugiakampį", + "continue-draw-polygon": "Tęsti daugiakampio piešimą", + "finish-draw-polygon": "Baigti piešti daugiakampį", + "start-draw-circle": "Pradėti piešti apskritimą", + "finish-draw-circle": "Baigti piešti apskritimą" + } + }, + "widgets-bundle": { + "current": "Dabartinis rinkinys", + "widgets-bundles": "Valdiklių rinkiniai", + "widgets-bundle-widgets": "Valdiklių rinkinio valdikliai", + "add": "Pridėti valdiklių rinkinį", + "delete": "Panaikinti valdiklių rinkinį", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "description": "Aprašymas", + "image-preview": "Paveiksliuko peržiūra", + "scada": "SCADA valdiklių rinkinys", + "order": "Eiliškumas", + "add-widgets-bundle-text": "Pridėti naują valdiklių rinkinį", + "no-widgets-bundles-text": "Valdiklių rinkinių nėra", + "empty": "Valdiklių rinkinys tuščias", + "details": "Informacija", + "widgets-bundle-details": "Informacija apie valdiklių rinkinį", + "delete-widgets-bundle-title": "Ar tikrai norite panaikinti valdiklių rinkinį '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Būkite dėmesingi — po patvirtinimo valdiklių rinkinys ir visa su juo susijusi informacija bus pašalinta.", + "delete-widgets-bundles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }?", + "delete-widgets-bundles-action-title": "Panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }", + "delete-widgets-bundles-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti valdiklių rinkiniai ir su jais susijusi informacija bus pašalinti.", + "no-widgets-bundles-matching": "Valdiklių rinkinių, atitinkančių '{{widgetsBundle}}', nėra.", + "widgets-bundle-required": "Valdiklių rinkinys būtinas.", + "system": "Sisteminis", + "import": "Importuoti valdiklių rinkinį", + "export": "Eksportuoti valdiklių rinkinį", + "export-widgets-bundle-widgets-prompt": "Įtraukti valdiklių tipus į eksportuotus duomenis (kitu atveju bus eksportuojami tik nurodytų valdiklių FQN identifikatoriai)", + "export-failed-error": "Valdiklių rinkinio eksportuoti nepavyko: {{error}}", + "create-new-widgets-bundle": "Sukurti naują valdiklių rinkinį", + "widgets-bundle-file": "Valdiklių rinkinio failas", + "invalid-widgets-bundle-file-error": "Valdiklių rinkinio importuoti nepavyko: neteisinga duomenų struktūra.", + "search": "Valdiklių rinkinių paieška", + "selected-widgets-bundles": "Pasirinkta { count, plural, =1 {1 valdiklių rinkinys} other {# valdiklių rinkiniai} }", + "open-widgets-bundle": "Atverti valdiklių rinkinį", + "loading-widgets-bundles": "Įkeliamas valdiklių rinkinys...", + "create-new": "Sukurti naują valdiklių rinkinį" + }, + "widget-config": { + "data": "Duomenys", + "settings": "Nustatymai", + "advanced": "Išplėstiniai nustatymai", + "appearance": "Išvaizda", + "widget-card": "Valdiklio kortelė", + "mobile": "Mobilusis vaizdas", + "title": "Pavadinimas", + "title-tooltip": "Pavadinimo paaiškinimas", + "general-settings": "Pagrindiniai nustatymai", + "display-title": "Rodyti valdiklio pavadinimą", + "card-title": "Kortelės pavadinimas", + "drop-shadow": "Šešėlis", + "enable-fullscreen": "Įjungti rodymą per visą ekraną", + "background-color": "Fono spalva", + "text-color": "Teksto spalva", + "border-radius": "Rėmelio apvalinimas", + "padding": "Vidinis atstumas (padding)", + "margin": "Išorinis atstumas (margin)", + "widget-style": "Valdiklio stilius", + "widget-css": "Valdiklio CSS stilius", + "title-style": "Antraštės stilius", + "mobile-mode-settings": "Mobiliojo režimo nustatymai", + "order": "Rikiavimo tvarka", + "height": "Aukštis", + "mobile-hide": "Slėpti valdiklį mobiliajame režime", + "desktop-hide": "Slėpti valdiklį darbalaukio režime", + "units": "Papildomas simbolis šalia reikšmės", + "units-by-default": "Numatytieji matavimo vienetai", + "decimals": "Skaitmenys po kablelio", + "decimals-by-default": "Numatytieji dešimtainiai skaitmenys", + "default-data-key-parameter-hint": "Šis parametras taikomas visoms valdiklio reikšmėms, nebent jis perrašomas konkretaus duomenų rakto konfigūracijoje", + "units-short": "Vienetai", + "decimals-short": "Dešimtainiai", + "decimals-suffix": "dešimtainiai skaitmenys", + "digits-suffix": "skaitmenys", + "timewindow": "Laikotarpio langas", + "use-dashboard-timewindow": "Naudoti skydelio laikotarpio langą", + "use-widget-timewindow": "Naudoti valdiklio laikotarpio langą", + "display-timewindow": "Rodyti laikotarpio langą", + "legend": "Legenda", + "display-legend": "Rodyti legendą", + "datasources": "Duomenų šaltiniai", + "datasource": "Duomenų šaltinis", + "maximum-datasources": "Daugiausiai leidžiama { count, plural, =1 {1 duomenų šaltinis.} other {# duomenų šaltiniai} }", + "timeseries-key-error": "Reikia nurodyti bent vieną telemetrijos rodiklį", + "datasource-type": "Tipas", + "datasource-parameters": "Parametrai", + "remove-datasource": "Pašalinti duomenų šaltinį", + "add-datasource": "Pridėti duomenų šaltinį", + "target-device": "Tikslinis įrenginys", + "alarm-source": "Įspėjimo šaltinis", + "actions": "Veiksmai", + "action": "Veiksmas", + "add-action": "Pridėti veiksmą", + "search-actions": "Veiksmų paieška", + "no-actions-text": "Veiksmų nėra", + "action-source": "Veiksmo šaltinis", + "select-action-source": "Pasirinkite veiksmo šaltinį", + "action-source-required": "Veiksmo šaltinis būtinas.", + "column-index": "Stulpelio indeksas", + "select-column-index": "Pasirinkite stulpelio indeksą", + "column-index-required": "Stulpelio indeksas būtinas.", + "not-set": "Nenurodyta", + "action-name": "Veiksmo pavadinimas", + "action-name-required": "Veiksmo pavadinimas būtinas.", + "action-name-not-unique": "Veiksmas su tokiu pavadinimu jau egzistuoja.
    To paties šaltinio veiksmų pavadinimai negali kartotis.", + "action-icon": "Piktograma", + "header-button": { + "button-settings": "Mygtuko nustatymai", + "button-type": "Mygtuko tipas", + "button-type-basic": "Paprastas", + "button-type-raised": "Iškeltas (Raised)", + "button-type-stroked": "Kontūrinis (Stroked)", + "button-type-flat": "Plokščias (Flat)", + "button-type-icon": "Piktogramos mygtukas", + "button-type-mini-fab": "Mini FAB (Floating Action Button)", + "colors": "Spalvos", + "color": "Spalva", + "background": "Fonas", + "border": "Rėmelis", + "advanced-button-style": "Išplėstinis mygtuko stilius", + "button-style": "Mygtuko stilius" + }, + "show-hide-action-using-function": "Rodyti / slėpti veiksmą pagal funkciją", + "show-action-function": "Veiksmo rodymo funkcija", + "action-type": "Veiksmo tipas", + "action-type-required": "Veiksmo tipas būtinas.", + "edit-action": "Redaguoti veiksmą", + "delete-action": "Panaikinti veiksmą", + "delete-action-title": "Panaikinti valdiklio veiksmą", + "delete-action-text": "Ar tikrai norite panaikinti valdiklio veiksmą '{{actionName}}'?", + "title-icon": "Antraštės piktograma", + "display-icon": "Rodyti antraštės piktogramą", + "card-icon": "Kortelės piktograma", + "icon": "Piktograma", + "icon-color": "Piktogramos spalva", + "icon-size": "Piktogramos dydis", + "advanced-settings": "Išplėstiniai nustatymai", + "data-settings": "Duomenų nustatymai", + "limits": "Ribos", + "no-data-display-message": "Alternatyvus tekstas, kai nėra duomenų", + "data-page-size": "Maksimalus subjektų skaičius viename duomenų šaltinyje", + "settings-component-not-found": "Nerasta nustatymų formos komponento su selektoriumi '{{selector}}'", + "preview": "Peržiūra", + "set": "Nustatyti", + "set-message": "Nustatyti pranešimą", + "advanced-title-style": "Išplėstinis antraštės stilius", + "card-style": "Kortelės stilius", + "text": "Tekstas", + "background": "Fonas", + "advanced-widget-style": "Išplėstinis valdiklio stilius", + "card-buttons": "Kortelės mygtukai", + "show-card-buttons": "Rodyti kortelės mygtukus", + "card-border-radius": "Kortelės kampų apvalinimas", + "card-padding": "Kortelės vidinis atstumas (padding)", + "card-appearance": "Kortelės išvaizda", + "color": "Spalva", + "tooltip": "Paaiškinimas (tooltip)", + "units-required": "Matavimo vienetas būtinas.", + "list-layout": "Sąrašo išdėstymas", + "layout": "Išdėstymas", + "resize-options": "Dydžio keitimo parinktys", + "resizable": "Keičiamo dydžio", + "preserve-aspect-ratio": "Išlaikyti proporcijas" + }, + "widget-type": { + "import": "Importuoti valdiklį", + "export": "Eksportuoti valdiklį", + "export-failed-error": "Valdiklio eksportuoti nepavyko: {{error}}", + "widget-file": "Valdiklio failas", + "invalid-widget-file-error": "Nepavyko importuoti valdiklio: neteisinga valdiklio duomenų struktūra." + }, + "markdown": { + "edit": "Redaguoti", + "preview": "Peržiūra", + "copy-code": "Spustelėkite, kad nukopijuotumėte", + "copied": "Nukopijuota!" + }, + "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "Konfigūracija priklauso nuo mobiliosios aplikacijos QR kodo valdiklio pagrindiniuose platformos nustatymuose", + "get-it-on-google-play": "Atsisiųsti iš Google Play", + "download-on-the-app-store": "Atsisiųsti iš App Store" + }, + "action-button": { + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, kuris įvykdomas paspaudus mygtuką", + "first-button-click": "Pirmas mygtuko paspaudimas", + "first-button-click-hint": "Veiksmas, vykdomas pirmo mygtuko paspaudimo metu.", + "second-button-click": "Antras mygtuko paspaudimas", + "second-button-click-hint": "Veiksmas, vykdomas antro mygtuko paspaudimo metu.", + "button-click-hint": "Veiksmas, vykdomas paspaudus valdiklį." + }, + "command-button": { + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, atliekamas paspaudus mygtuką." + }, + "power-button": { + "behavior": "Elgsena", + "power-on": "Įjungti", + "power-on-hint": "Veiksmas, atliekamas įjungiant komponentą.", + "power-off": "Išjungti", + "power-off-hint": "Veiksmas, atliekamas išjungiant komponentą.", + "on-label": "Įjungta", + "off-label": "Išjungta", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-simplified": "Supaprastintas", + "layout-outlined": "Aptvertas", + "layout-default-volume": "Numatytasis.Garsas", + "layout-simplified-volume": "Supaprastintas.Garsas", + "layout-outlined-volume": "Aptvertas.Garsas", + "layout-default-icon": "Numatytasis.Piktograma", + "layout-simplified-icon": "Supaprastintas.Piktograma", + "layout-outlined-icon": "Aptvertas.Piktograma", + "main": "Pagrindinis", + "background": "Fonas", + "button-icon-on": "Mygtuko piktograma 'Įjungta'", + "button-icon-off": "Mygtuko piktograma 'Išjungta'", + "power-on-colors": "Įjungimo spalvos", + "power-off-colors": "Išjungimo spalvos", + "disabled-colors": "Išjungtos spalvos", + "button": "Mygtukas" + }, + "toggle-button": { + "behavior": "Elgsena", + "checked": "Pažymėta", + "unchecked": "Nepažymėta", + "check": "Pažymėti", + "check-hint": "Veiksmas, atliekamas pažymint komponentą.", + "uncheck": "Nuimti žymėjimą", + "uncheck-hint": "Veiksmas, atliekamas nuimant žymėjimą nuo komponento.", + "auto-scale": "Automatinis mastelis", + "horizontal-fill": "Horizontalus užpildymas", + "vertical-fill": "Vertikalus užpildymas", + "button-appearance": "Mygtuko išvaizda" + }, + "segmented-button": { + "layout": "Išdėstymas", + "layout-squared": "Kampuotas", + "layout-rounded": "Apvalintas", + "card-border": "Kortelės rėmelis", + "button-appearance": "Mygtuko išvaizda", + "first": "Pirmas", + "second": "Antras", + "color-styles": "Spalvų stiliai", + "selected": "Pasirinkta", + "unselected": "Nepasirinkta" + }, + "button": { + "layout": "Išdėstymas", + "outlined": "Aptvertas", + "filled": "Užpildytas", + "underlined": "Pabrauktas", + "basic": "Paprastas", + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "border-radius": "Kampų apvalinimas", + "color-palette": "Spalvų paletė", + "main": "Pagrindinis", + "background": "Fonas", + "border": "Rėmelis", + "custom-styles": "Tinkinti stiliai", + "clear-style": "Išvalyti stilių", + "shadow": "Šešėlis", + "enabled": "Įjungta", + "disabled": "Išjungta", + "preview": "Peržiūra", + "copy-style-from": "Kopijuoti stilių iš" + }, + "value-stepper": { + "behavior": "Elgsena", + "simplified": "Supaprastintas", + "filled": "Užpildytas", + "outlined": "Aptvertas", + "volume": "Garsas", + "initial-state": "Pradinė būsena", + "initial-state-hint": "Veiksmas, skirtas gauti pradinę reikšmę.", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Nustatykite sąlygą, kada komponentas yra išjungiamas.", + "right-button-click": "Dešinio mygtuko paspaudimas", + "right-button-click-hint": "Veiksmas paspaudus dešinį mygtuką.", + "left-button-click": "Kairio mygtuko paspaudimas", + "left-button-click-hint": "Veiksmas paspaudus kairį mygtuką.", + "auto-scale": "Automatinis mastelis", + "value-range": "Reikšmių intervalas", + "min-range": "Minimali reikšmė", + "max-range": "Maksimali reikšmė", + "value-increment-decrement-step": "Reikšmės didinimo/mažinimo žingsnis", + "value": "Reikšmė", + "value-box-background": "Reikšmės laukelio fonas", + "border": "Rėmelis", + "button-appearance": "Mygtuko išvaizda", + "left": "Kairė", + "right": "Dešinė", + "left-button": "Kairysis mygtukas", + "right-button": "Dešinysis mygtukas", + "icon": "Piktograma", + "color-palette": "Spalvų paletė", + "main": "Pagrindinis", + "background": "Fonas", + "button-icon-on": "Mygtuko piktograma 'Įjungta'", + "button-on-colors": "Įjungimo spalvos", + "disabled-colors": "Išjungtos spalvos" + }, + "button-state": { + "activated-state": "Aktyvuota būsena", + "activated-state-hint": "Nustatykite sąlygą, kada mygtukas yra aktyvus.", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Nustatykite sąlygą, kada mygtukas yra išjungiamas.", + "selected-state": "Pasirinkta būsena", + "selected-state-hint": "Nustatykite sąlygą, kada mygtukas yra pasirinktas.", + "enabled": "Įjungta", + "hovered": "Užvestas", + "pressed": "Paspaustas", + "activated": "Aktyvuotas", + "disabled": "Išjungtas", + "initial": "Pirmasis mygtukas", + "first": "Pirmas", + "second": "Antras" + }, + "background": { + "background": "Fonas", + "background-settings": "Fono nustatymai", + "background-type-image": "Įkelti paveikslėlį", + "background-type-color": "Vientisa spalva", + "image-url": "Paveikslėlio URL", + "overlay": "Perdanga", + "enable-overlay": "Įjungti perdangą", + "blur": "Suliejimas", + "preview": "Peržiūra" + }, + "bar-chart": { + "bar-appearance": "Stulpelio išvaizda", + "label-on-bar": "Etiketė ant stulpelio", + "value-on-bar": "Reikšmė ant stulpelio", + "bar-chart-style": "Stulpelinės diagramos stilius", + "bar-axis": "Stulpelio ašis" + }, + "polar-area-chart": { + "polar-axis": "Polinė ašis", + "start-angle": "Pradinis kampas", + "polar-area-chart-style": "Polinės diagramos stilius" + }, + "battery-level": { + "layout": "Išdėstymas", + "layout-vertical-solid": "Vertikalus. Vientisas", + "layout-horizontal-solid": "Horizontalus. Vientisas", + "layout-vertical-divided": "Vertikalus. Padalintas", + "layout-horizontal-divided": "Horizontalus. Padalintas", + "icon": "Piktograma", + "value": "Reikšmė", + "auto-scale": "Automatinis mastelis", + "battery-level-color": "Baterijos lygio spalva", + "battery-shape-color": "Baterijos formos spalva", + "battery-level-card-style": "Baterijos lygio kortelės stilius", + "sections-count": "Sekcijų skaičius" + }, + "signal-strength": { + "value": "Reikšmė", + "last-update": "Paskutinis atnaujinimas", + "no-signal": "Nėra signalo", + "layout": "Išdėstymas", + "layout-wifi": "Wi-Fi", + "layout-cellular-bar": "Mobiliojo ryšio stulpeliai", + "icon": "Piktograma", + "date": "Data", + "active-bars-color": "Aktyvių signalo stulpelių spalva", + "inactive-bars-color": "Neaktyvių signalo stulpelių spalva", + "signal-strength-card-style": "Signalo stiprumo kortelės stilius", + "no-signal-rssi-value": "\"Nėra signalo\" RSSI reikšmė" + }, + "status-widget": { + "behavior": "Elgsena", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-center": "Centruotas", + "layout-icon": "Piktograma", + "on": "Įjungta", + "off": "Išjungta", + "label": "Etiketė", + "status": "Būsena", + "icon": "Piktograma", + "color-palette": "Spalvų paletė", + "disabled-color-palette": "Išjungta spalvų paletė", + "primary": "Pagrindinė", + "primary-color-hint": "Piktogramos ir etiketės spalva", + "secondary": "Antrinė", + "secondary-color-hint": "Būsenos spalva", + "background": "Fonas" + }, + "chart": { + "common-settings": "Bendrieji nustatymai", + "enable-stacking-mode": "Įjungti kaupimo režimą", + "selection": "Laiko intervalo pasirinkimas", + "enable-selection-mode": "Įjungti pasirinkimo režimą", + "line-shadow-size": "Linijos šešėlio dydis", + "display-smooth-lines": "Rodyti glotnias (lenktas) linijas", + "default-bar-width": "Numatytasis stulpelio plotis neagreguotiems duomenims (milisekundėmis)", + "bar-alignment": "Stulpelių lygiavimas", + "bar-alignment-left": "Kairėje", + "bar-alignment-right": "Dešinėje", + "bar-alignment-center": "Centre", + "default-font": "Numatytasis šriftas", + "default-font-size": "Numatytasis šrifto dydis", + "default-font-color": "Numatytoji šrifto spalva", + "thresholds-line-width": "Numatytasis ribų linijos plotis", + "tooltip-settings": "Užuominos nustatymai", + "tooltip": "Užuomina", + "show-tooltip": "Rodyti užuominą", + "hover-individual-points": "Užvesti virš atskirų taškų", + "show-cumulative-values": "Rodyti sumines reikšmes kaupimo režime", + "hide-zero-false-values": "Slėpti nulinės/neteisingos reikšmės užuominose", + "tooltip-value-format-function": "Užuominos reikšmės formatavimo funkcija", + "grid-settings": "Tinklelio nustatymai", + "show-vertical-lines": "Rodyti vertikalias linijas", + "show-horizontal-lines": "Rodyti horizontalias linijas", + "grid-outline-border-width": "Tinklelio kontūro/rėmelio plotis (px)", + "primary-color": "Pagrindinė spalva", + "background-color": "Fono spalva", + "ticks-color": "Atskirų padalų spalva", + "xaxis-settings": "X ašies nustatymai", + "axis-title": "Ašies pavadinimas", + "xaxis-tick-labels-settings": "X ašies padalų etikečių nustatymai", + "show-tick-labels": "Rodyti ašies padalų etiketes", + "yaxis-settings": "Y ašies nustatymai", + "min-scale-value": "Minimalioji skalės reikšmė", + "max-scale-value": "Maksimalioji skalės reikšmė", + "yaxis-tick-labels-settings": "Y ašies padalų etikečių nustatymai", + "tick-step-size": "Padalų žingsnio dydis", + "number-of-decimals": "Rodyti dešimtainių skaičių kiekis", + "ticks-formatter-function": "Padalų formatavimo funkcija", + "comparison-settings": "Palyginimo nustatymai", + "enable-comparison": "Įjungti palyginimą", + "time-for-comparison": "Palyginimo laikotarpis", + "time-for-comparison-previous-interval": "Ankstesnis intervalas (numatytasis)", + "time-for-comparison-days": "Prieš dieną", + "time-for-comparison-weeks": "Prieš savaitę", + "time-for-comparison-months": "Prieš mėnesį", + "time-for-comparison-years": "Prieš metus", + "time-for-comparison-custom-interval": "Pasirinktinis intervalas", + "custom-interval-value": "Pasirinktinis intervalo dydis (ms)", + "comparison-x-axis-settings": "Palyginimo X ašies nustatymai", + "axis-position": "Ašies padėtis", + "axis-position-top": "Viršuje (numatyta)", + "axis-position-bottom": "Apačioje", + "custom-legend-settings": "Tinkintos legendos nustatymai", + "enable-custom-legend": "Įjungti tinkintą legendą (leidžia naudoti atributų/laikinių eilučių reikšmes etiketėse)", + "key-name": "Raktinio parametro pavadinimas", + "key-name-required": "Būtina nurodyti raktinio parametro pavadinimą", + "key-type": "Rakto tipas", + "key-type-attribute": "Atributas", + "key-type-timeseries": "Laikinė eilutė", + "label-keys-list": "Etiketėse naudojamų raktų sąrašas", + "no-label-keys": "Nesukonfigūruoti jokie raktai", + "add-label-key": "Pridėti naują raktą", + "line-width": "Linijos plotis", + "color": "Spalva", + "data-is-hidden-by-default": "Duomenys pagal numatymą paslėpti", + "disable-data-hiding": "Išjungti duomenų slėpimą", + "remove-from-legend": "Pašalinti duomenų raktą iš legendos", + "exclude-from-stacking": "Pašalinti iš kaupimo (prieinama tik „Kaupimo“ režime)", + "line-settings": "Linijos nustatymai", + "show-line": "Rodyti liniją", + "fill-line": "Užpildyti liniją", + "fill-line-opacity": "Užpildymo nepermatomumas", + "points-settings": "Taškų nustatymai", + "show-points": "Rodyti taškus", + "points-line-width": "Taškų kontūro plotis", + "points-radius": "Taškų spindulys", + "point-shape": "Taško forma", + "point-shape-circle": "Apskritimas", + "point-shape-cross": "Kryžius", + "point-shape-diamond": "Deimantas", + "point-shape-square": "Kvadratas", + "point-shape-triangle": "Trikampis", + "point-shape-custom": "Tinkinta funkcija", + "point-shape-draw-function": "Taško piešimo funkcija", + "show-separate-axis": "Rodyti atskirą ašį", + "axis-position-left": "Kairėje", + "axis-position-right": "Dešinėje", + "thresholds": "Ribos", + "no-thresholds": "Ribos nesukonfigūruotos", + "add-threshold": "Pridėti ribą", + "show-values-for-comparison": "Rodyti istorines reikšmes palyginimui", + "comparison-values-label": "Istorinių reikšmių etiketė", + "comparison-line-color": "Palyginimo linijos spalva", + "threshold-settings": "Ribų nustatymai", + "use-as-threshold": "Naudoti rakto reikšmę kaip ribą", + "threshold-line-width": "Ribos linijos plotis", + "threshold-color": "Ribos spalva", + "common-pie-settings": "Bendrieji skritulinės diagramos nustatymai", + "radius": "Spindulys", + "inner-radius": "Vidinis spindulys", + "tilt": "Pasvirimas", + "common-pie-settings-range-error": "Reikšmė turi būti nuo 0 iki 1", + "stroke-settings": "Kontūro nustatymai", + "width-pixels": "Plotis (pikseliais)", + "show-labels": "Rodyti etiketes", + "animation-settings": "Animacijos nustatymai", + "animated-pie": "Įjungti skritulinės diagramos animaciją (eksperimentinė funkcija)", + "border-settings": "Rėmelio nustatymai", + "border-width": "Rėmelio plotis", + "border-color": "Rėmelio spalva", + "legend-settings": "Legendos nustatymai", + "display-legend": "Rodyti legendą", + "labels-font-color": "Etikečių šrifto spalva", + "series": "Serijos", + "add-series": "Pridėti seriją", + "series-settings": "Serijos nustatymai", + "remove-series": "Pašalinti seriją", + "no-series": "Nesukonfigūruota jokia serija", + "no-series-error": "Būtina nurodyti bent vieną seriją", + "chart-appearance": "Diagramos išvaizda", + "vertical-grid-lines": "Vertikalios tinklelio linijos", + "horizontal-grid-lines": "Horizontalios tinklelio linijos", + "chart-background": "Diagramos fonas", + "grid-lines-color": "Tinklelio linijų spalva", + "border": "Rėmelis", + "axis": "Ašis", + "vertical-axis": "Vertikali ašis", + "ticks": "Padalos", + "horizontal-axis": "Horizontali ašis", + "shape-empty-circle": "Tuščias apskritimas", + "shape-circle": "Apskritimas", + "shape-rect": "Stačiakampis", + "shape-round-rect": "Apvalintų kampų stačiakampis", + "shape-triangle": "Trikampis", + "shape-diamond": "Deimantas", + "shape-pin": "Žymeklis", + "shape-arrow": "Rodyklė", + "shape-none": "Nėra", + "line-type-solid": "Vientisa", + "line-type-dashed": "Brūkšniuota", + "line-type-dotted": "Taškuota", + "label-position-top": "Viršuje", + "label-position-bottom": "Apačioje", + "label-position-outside": "Išorėje", + "label-position-inside": "Viduje", + "fill": "Užpildas", + "fill-type-none": "Nėra", + "fill-type-solid": "Vientisas", + "fill-type-opacity": "Permatomumas", + "fill-type-gradient": "Gradientas", + "background": "Fonas", + "opacity": "Permatomumas", + "gradient-stops": "Gradiento taškai", + "gradient-start": "pradžia", + "gradient-end": "pabaiga", + "animation": { + "animation": "Animacija", + "animation-threshold": "Animacijos riba", + "animation-duration": "Animacijos trukmė", + "animation-easing": "Animacijos švelninimas", + "animation-delay": "Animacijos delsimas", + "update-animation-duration": "Atnaujinimo animacijos trukmė", + "update-animation-easing": "Atnaujinimo animacijos švelninimas", + "update-animation-delay": "Atnaujinimo animacijos delsimas" + }, + "chart-axis": { + "scale": "Skalė", + "scale-min": "min", + "scale-max": "maks", + "scale-auto": "Automatinė" + }, + "bar": { + "show-border": "Rodyti rėmelį", + "border-width": "Rėmelio plotis", + "border-radius": "Kampų apvalinimas", + "bar-width": "Stulpelio plotis", "label": "Etiketė", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti įrenginio Id", - "copyAccessToken": "Kopijuoti prieigos raktą", - "copy-mqtt-authentication": "Copy MQTT credentials", - "idCopiedMessage": "Įrenginio Id nukopijuotas į iškarpinę", - "accessTokenCopiedMessage": "Įrenginio prieigos raktas nukopijuotas į iškarpinę", - "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard", - "assignedToCustomer": "Priskirtas klientui", - "unable-delete-device-alias-title": "Įrenginio pseudonimo panaikinti nepavyko", - "unable-delete-device-alias-text": "Įrenginio pseudonimas '{{deviceAlias}}' nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", - "is-gateway": "Is gateway", - "overwrite-activity-time": "Overwrite activity time for connected device", - "device-filter": "Įrenginių filtras", - "device-filter-title": "Įrenginių filtras", - "filter-title": "Filtras", - "device-state": "Įrenginio būsena", - "state": "Būsena", - "any": "Visos", - "active": "Aktyvus", - "inactive": "Neaktyvus", - "public": "Viešas", - "device-public": "Įrenginys yra viešas", - "select-device": "Pasirinkite įrenginį", - "select-group-to-add": "Pasirinkite grupę, į kurią norite pridėti pažymėtus įrenginius", - "select-group-to-move": "Pasirinkite grupę, į kurią norite perkelti pažymėtus įrenginius", - "remove-devices-from-group": "Ar tikrai norite iš grupės '{{entityGroup}}' pašalinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "group": "Įrenginių grupė", - "list-of-groups": "{ count, plural, =1 {Viena įrenginio grupė} other {# įrenginių grupių sąrašas} }", - "group-name-starts-with": "Įrenginių grupės, kurių pavadinimai prasideda '{{prefix}}'", - "import": "Importuoti įrenginį", - "device-file": "Įrenginio failas", - "search": "Įrenginių paieška", - "selected-devices": "Pasirinkta { count, plural, =1 {1 įrenginys} other {# įrenginiai} }", - "device-configuration": "Įrenginių konfigūracija", - "transport-configuration": "Transport configuration", - "wizard": { - "device-details": "Informacija apie įrenginį" - }, - "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?", - "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge.", - "time": "Time", - "connectivity": { - "check-connectivity": "Check connectivity", - "device-created-check-connectivity": "Device created. Let's check connectivity!", - "loading-check-connectivity-command": "Loading check connectivity commands...", - "use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell", - "execute-following-command": "Execute the following command", - "install-curl-windows": "Starting Windows 10 b17063, cURL is available by default", - "install-curl-macos": "Starting Mac OS X 10.2 6C115 (Jaguar), cURL is available by default", - "install-mqtt-windows": "Use the instructions to download, install, setup and run mosquitto_pub", - "install-coap-client": "Use the instructions to download, install, setup and run coap-client", - "install-necessary-client-tools": "Install necessary client tools", - "mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509", - "coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509", - "snmp-command": "Use the following documentation to connect the device through the SNMP.", - "sparkplug-command": "Use the following documentation to connect the device through the MQTT Sparkplug.", - "lwm2m-command": "Use the following documentation to connect the device through the LWM2M." - } + "label-hint": "Rodyti etiketę virš stulpelio.", + "series-label-hint": "Rodyti etiketę su reikšme virš stulpelio.", + "label-background": "Etiketės fonas" + } }, - "asset-profile": { - "asset-profile": "Profilis", - "asset-profiles": "Asset profiles", - "all-asset-profiles": "Visi", - "add": "Add asset profile", - "edit": "Edit asset profile", - "asset-profile-details": "Asset profile details", - "no-asset-profiles-text": "No asset profiles found", - "search": "Search asset profiles", - "selected-asset-profiles": "{ count, plural, =1 {1 asset profile} other {# asset profiles} } selected", - "no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.", - "asset-profile-required": "Asset profile is required", - "idCopiedMessage": "Asset profile Id has been copied to clipboard", - "set-default": "Make asset profile default", - "delete": "Delete asset profile", - "copyId": "Copy asset profile Id", - "name-max-length": "Name should be less than 256", - "new-device-profile-name": "Asset profile name", - "new-device-profile-name-required": "Asset profile name is required.", - "name": "Name", - "name-required": "Name is required.", - "image": "Asset profile image", - "description": "Description", - "default": "Default", - "default-rule-chain": "Default rule chain", - "default-edge-rule-chain": "Default edge rule chain", - "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for assets of this asset profile", - "mobile-dashboard": "Mobile dashboard", - "mobile-dashboard-hint": "Used by mobile application as a asset details dashboard", - "select-queue-hint": "Select from a drop-down list.", - "delete-asset-profile-title": "Are you sure you want to delete the asset profile '{{assetProfileName}}'?", - "delete-asset-profile-text": "Be careful, after the confirmation the asset profile and all related data will become unrecoverable.", - "delete-asset-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 asset profile} other {# asset profiles} }?", - "delete-asset-profiles-text": "Be careful, after the confirmation all selected asset profiles will be removed and all related data will become unrecoverable.", - "set-default-asset-profile-title": "Are you sure you want to make the asset profile '{{assetProfileName}}' default?", - "set-default-asset-profile-text": "After the confirmation the asset profile will be marked as default and will be used for new assets with no profile specified.", - "no-asset-profiles-found": "No asset profiles found.", - "create-new-asset-profile": "Create a new one!", - "create-asset-profile": "Create new asset profile", - "import": "Import asset profile", - "export": "Export asset profile", - "export-failed-error": "Unable to export asset profile: {{error}}", - "asset-profile-file": "Asset profile file", - "invalid-asset-profile-file-error": "Unable to import asset profile: Invalid asset profile data structure." - }, - "device-profile": { - "device-profile": "Profilis", - "device-profiles": "Device profiles", - "all-device-profiles": "Visi", - "add": "Add device profile", - "edit": "Edit device profile", - "device-profile-details": "Device profile details", - "no-device-profiles-text": "No device profiles found", - "search": "Search device profiles", - "selected-device-profiles": "{ count, plural, =1 {1 device profile} other {# device profiles} } selected", - "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", - "device-profile-required": "Device profile is required", - "idCopiedMessage": "Device profile Id has been copied to clipboard", - "set-default": "Make device profile default", - "delete": "Delete device profile", - "copyId": "Copy device profile Id", - "name-max-length": "Name should be less than 256", - "name": "Name", - "name-required": "Name is required.", - "type": "Profile type", - "type-required": "Profile type is required.", - "type-default": "Default", - "image": "Device profile image", - "transport-type": "Transport type", - "transport-type-required": "Transport type is required.", - "transport-type-default": "Default", - "transport-type-default-hint": "Supports basic MQTT, HTTP and CoAP transport", - "transport-type-mqtt": "MQTT", - "transport-type-mqtt-hint": "Enables advanced MQTT transport settings", - "transport-type-coap": "CoAP", - "transport-type-coap-hint": "Enables advanced CoAP transport settings", - "transport-type-lwm2m": "LWM2M", - "transport-type-lwm2m-hint": "LWM2M transport type", - "transport-type-snmp": "SNMP", - "transport-type-snmp-hint": "Specify SNMP transport configuration", - "transport-type-http": "HTTP", - "description": "Description", - "default": "Default", - "profile-configuration": "Profile configuration", - "transport-configuration": "Transport configuration", - "default-rule-chain": "Default rule chain", - "default-edge-rule-chain": "Default edge rule chain", - "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for devices of this device profile", - "mobile-dashboard": "Mobile dashboard", - "mobile-dashboard-hint": "Used by mobile application as a device details dashboard", - "select-queue-hint": "Select from a drop-down list.", - "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", - "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data including associated OTA updates will become unrecoverable.", - "delete-device-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 device profile} other {# device profiles} }?", - "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data including associated OTA updates will become unrecoverable.", - "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", - "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", - "no-device-profiles-found": "No device profiles found.", - "create-new-device-profile": "Create a new one!", - "mqtt-device-topic-filters": "MQTT device topic filters", - "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", - "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) node.", - "mqtt-device-topic-filters-spark-plug-hint": "Allow connections from EoN nodes with Sparkplug B payload and topic format.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metrics to store as attributes.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Names of SparkPlug metrics that will be stored as device attributes. All other metrics will be stored as device telemetry.", - "mqtt-device-payload-type": "MQTT device payload", - "mqtt-device-payload-type-json": "JSON", - "mqtt-device-payload-type-proto": "Protobuf", - "mqtt-enable-compatibility-with-json-payload-format": "Enable compatibility with other payload formats.", - "mqtt-enable-compatibility-with-json-payload-format-hint": "When enabled, the platform will use a Protobuf payload format by default. If parsing fails, the platform will attempt to use JSON payload format. Useful for backward compatibility during firmware updates. For example, the initial release of the firmware uses Json, while the new release uses Protobuf. During the process of firmware update for the fleet of devices, it is required to support both Protobuf and JSON simultaneously. The compatibility mode introduces slight performance degradation, so it is recommended to disable this mode once all devices are updated.", - "mqtt-use-json-format-for-default-downlink-topics": "Use Json format for default downlink topics", - "mqtt-use-json-format-for-default-downlink-topics-hint": "When enabled, the platform will use Json payload format to push attributes and RPC via the following topics: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. This setting does not impact attribute and rpc subscriptions sent using new (v2) topics: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id is an integer request identifier.", - "mqtt-send-ack-on-validation-exception": "Send PUBACK on PUBLISH message validation failure", - "mqtt-send-ack-on-validation-exception-hint": "By default, the platform will close the MQTT session on message validation failure. When enabled, the platform will send publish acknowledgment instead of closing the session.", - "snmp-add-mapping": "Add SNMP mapping", - "snmp-mapping-not-configured": "No mapping for OID to timeseries/telemetry configured", - "snmp-timseries-or-attribute-name": "Timeseries/attribute name for mapping", - "snmp-timseries-or-attribute-type": "Timeseries/attribute type for mapping", - "snmp-method-pdu-type-get-request": "GetRequest", - "snmp-method-pdu-type-get-next-request": "GetNextRequest", - "snmp-oid": "OID", - "transport-device-payload-type-json": "JSON", - "transport-device-payload-type-proto": "Protobuf", - "mqtt-payload-type-required": "Payload type is required.", - "coap-device-type": "CoAP device type", - "coap-device-payload-type": "CoAP device payload", - "coap-device-type-required": "CoAP device type is required.", - "coap-device-type-default": "Default", - "coap-device-type-efento": "Efento NB-IoT", - "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", - "telemetry-topic-filter": "Telemetry topic filter", - "telemetry-topic-filter-required": "Telemetry topic filter is required.", - "attributes-topic-filter": "Attributes publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", - "telemetry-proto-schema": "Telemetry proto schema", - "telemetry-proto-schema-required": "Telemetry proto schema is required.", - "attributes-proto-schema": "Attributes proto schema", - "attributes-proto-schema-required": "Attributes proto schema is required.", - "rpc-response-proto-schema": "RPC response proto schema", - "rpc-response-proto-schema-required": "RPC response proto schema is required.", - "rpc-response-topic-filter": "RPC response topic filter", - "rpc-response-topic-filter-required": "RPC response topic filter is required.", - "rpc-request-proto-schema": "RPC request proto schema", - "rpc-request-proto-schema-required": "RPC request proto schema is required.", - "rpc-request-proto-schema-hint": "RPC request message should always have fields: string method = 1; int32 requestId = 2; and params = 3 of any data type.", - "not-valid-pattern-topic-filter": "Not valid pattern topic filter", - "not-valid-single-character": "Invalid use of a single-level wildcard character", - "not-valid-multi-character": "Invalid use of a multi-level wildcard character", - "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", - "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", - "alarm-rules": "Alarm rules", - "alarm-rules-with-count": "Alarm rules ({{count}})", - "no-alarm-rules": "No alarm rules configured", - "add-alarm-rule": "Add alarm rule", - "edit-alarm-rule": "Edit alarm rule", - "alarm-type": "Alarm type", - "alarm-type-required": "Alarm type is required.", - "alarm-type-unique": "Alarm type must be unique within the device profile alarm rules.", - "alarm-type-max-length": "Alarm type should be less than 256", - "create-alarm-pattern": "Create {{alarmType}} alarm", - "create-alarm-rules": "Create alarm rules", - "no-create-alarm-rules": "No create conditions configured", - "add-create-alarm-rule-prompt": "Please add create alarm rule", - "clear-alarm-rule": "Clear alarm rule", - "no-clear-alarm-rule": "No clear condition configured", - "add-create-alarm-rule": "Add create condition", - "add-clear-alarm-rule": "Add clear condition", - "select-alarm-severity": "Select alarm severity", - "alarm-severity-required": "Alarm severity is required.", - "condition-duration": "Condition duration", - "condition-duration-value": "Duration value", - "condition-duration-time-unit": "Time unit", - "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", - "condition-duration-value-pattern": "Duration value should be integers.", - "condition-duration-value-required": "Duration value is required.", - "condition-duration-time-unit-required": "Time unit is required.", - "advanced-settings": "Advanced settings", - "alarm-rule-additional-info": "Papildoma informacija", - "edit-alarm-rule-additional-info": "Redaguoti papildomą informaciją", - "alarm-rule-additional-info-placeholder": "Pateikite savo komentarus ir patikslinimus čia, kad jie būtų rodomi pavojaus signalo detalių skiltyje „Papildoma informacija“.", - "alarm-rule-additional-info-hint": "Hint: use ${keyName} to substitute values of the attribute or telemetry keys that are used in alarm rule condition.", - "alarm-rule-mobile-dashboard": "Mobile dashboard", - "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", - "alarm-rule-no-mobile-dashboard": "No dashboard selected", - "propagate-alarm": "Propagate alarm to related entities", - "alarm-rule-relation-types-list": "Relation types to propagate", - "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", - "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", - "propagate-alarm-to-owner-hierarchy": "Propagate alarm to entity owners hierarchy", - "propagate-alarm-to-tenant": "Propagate alarm to Tenant", - "alarm-rule-condition": "Alarm rule condition", - "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", - "edit-alarm-rule-condition": "Edit alarm rule condition", - "device-provisioning": "Device provisioning", - "provision-strategy": "Provision strategy", - "provision-strategy-required": "Provision strategy is required.", - "provision-strategy-disabled": "Disabled", - "provision-strategy-created-new": "Allow to create new devices", - "provision-strategy-check-pre-provisioned": "Check for pre-provisioned devices", - "provision-device-key": "Provision device key", - "provision-device-key-required": "Provision device key is required.", - "copy-provision-key": "Copy provision key", - "provision-key-copied-message": "Provision key has been copied to clipboard", - "provision-device-secret": "Provision device secret", - "provision-device-secret-required": "Provision device secret is required.", - "copy-provision-secret": "Copy provision secret", - "provision-secret-copied-message": "Provision secret has been copied to clipboard", - "provision-strategy-x509": { - "certificate-chain": "X509 Certificates Chain", - "certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.", - "allow-create-new-devices": "Create new devices", - "allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.", - "certificate-value": "Certificate in PEM format", - "certificate-value-required": "Certificate in PEM format is required", - "cn-regex-variable": "CN Regular Expression variable", - "cn-regex-variable-required": "CN Regular Expression variable is required", - "cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name." - }, - "condition": "Condition", - "condition-type": "Condition type", - "condition-type-simple": "Simple", - "condition-type-duration": "Duration", - "condition-during": "During {{during}}", - "condition-during-dynamic": "During \"{{ attribute }}\" ({{during}})", - "condition-type-repeating": "Repeating", - "condition-type-required": "Condition type is required.", - "condition-repeating-value": "Count of events", - "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", - "condition-repeating-value-pattern": "Count of events should be integers.", - "condition-repeating-value-required": "Count of events is required.", - "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", - "condition-repeat-times-dynamic": "Repeats \"{ attribute }\" ({ count, plural, =1 {1 time} other {# times} })", - "schedule-type": "Scheduler type", - "schedule-type-required": "Scheduler type is required.", - "schedule": "Schedule", - "edit-schedule": "Edit alarm schedule", - "schedule-any-time": "Active all the time", - "schedule-specific-time": "Active at a specific time", - "schedule-custom": "Custom", - "schedule-day": { - "monday": "Monday", - "tuesday": "Tuesday", - "wednesday": "Wednesday", - "thursday": "Thursday", - "friday": "Friday", - "saturday": "Saturday", - "sunday": "Sunday" - }, - "schedule-days": "Days", - "schedule-time": "Time", - "schedule-time-from": "From", - "schedule-time-to": "To", - "schedule-days-of-week-required": "At least one day of week should be selected.", - "create-device-profile": "Create new device profile", - "import": "Import device profile", - "export": "Export device profile", - "export-failed-error": "Unable to export device profile: {{error}}", - "device-profile-file": "Device profile file", - "invalid-device-profile-file-error": "Unable to import device profile: Invalid device profile data structure.", - "power-saving-mode": "Power Saving Mode", - "power-saving-mode-type": { - "default": "Use device profile power saving mode", - "psm": "Power Saving Mode (PSM)", - "drx": "Discontinuous Reception (DRX)", - "edrx": "Extended Discontinuous Reception (eDRX)" - }, - "edrx-cycle": "eDRX cycle", - "edrx-cycle-required": "eDRX cycle is required.", - "edrx-cycle-pattern": "eDRX cycle must be a positive integer.", - "edrx-cycle-min": "Minimum number of eDRX cycle is {{ min }} seconds.", - "paging-transmission-window": "Paging Transmission Window", - "paging-transmission-window-required": "Paging transmission window is required.", - "paging-transmission-window-pattern": "Paging transmission window must be a positive integer.", - "paging-transmission-window-min": "Minimum number ofpPaging transmission window is {{ min }} seconds.", - "psm-activity-timer": "PSM Activity Timer", - "psm-activity-timer-required": "PSM activity timer is required.", - "psm-activity-timer-pattern": "PSM activity timer must be a positive integer.", - "psm-activity-timer-min": "Minimum number of PSM activity timer is {{ min }} seconds.", - "lwm2m": { - "object-list": "Object list", - "object-list-empty": "No objects selected.", - "no-objects-found": "No objects found.", - "no-objects-matching": "No objects matching '{{object}}' were found.", - "model-tab": "LWM2M Model", - "add-new-instances": "Add new instances", - "instances-list": "Instances list", - "instances-list-required": "Instances list is required.", - "instance-id-pattern": "Instance id must be a positive integer.", - "instance-id-max": "Maximum instance id value {{max}}.", - "instance": "Instance", - "resource-label": "#ID Resource name", - "observe-label": "Observe", - "attribute-label": "Attribute", - "telemetry-label": "Telemetry", - "edit-observe-select": "To edit observe select telemetry or attribute", - "edit-attributes-select": "To edit attributes select telemetry or attribute", - "no-attributes-set": "No attributes set", - "key-name": "Key name", - "key-name-required": "Key name is required", - "attribute-name": "Name attribute", - "attribute-name-required": "Name attribute is required.", - "attribute-value": "Attribute value", - "attribute-value-required": "Attribute value is required.", - "attribute-value-pattern": "Attribute value must be a positive integer.", - "edit-attributes": "Edit attributes: {{ name }}", - "view-attributes": "View attributes: {{ name }}", - "add-attribute": "Add attribute", - "edit-attribute": "Edit attribute", - "view-attribute": "View attribute", - "remove-attribute": "Remove attribute", - "delete-server-text": "Be careful, after the confirmation the server configuration will become unrecoverable.", - "delete-server-title": "Are you sure you want to delete the server?", - "mode": "Security config mode", - "bootstrap-tab": "Bootstrap", - "bootstrap-server-legend": "Bootstrap Server (ShortId...)", - "lwm2m-server-legend": "LwM2M Server (ShortId...)", - "server": "Server", - "short-id": "Short server ID", - "short-id-tooltip": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.\nThe values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", - "short-id-required": "Short server ID is required.", - "short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.", - "short-id-pattern": "Short server ID must be a positive integer.", - "lifetime": "Client registration lifetime", - "lifetime-required": "Client registration lifetime is required.", - "lifetime-pattern": "Client registration lifetime must be a positive integer.", - "default-min-period": "Min period between two notifications (s)", - "default-min-period-tooltip": "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation.", - "default-min-period-required": "Minimum period is required.", - "default-min-period-pattern": "Minimum period must be a positive integer.", - "notification-storing": "Notification storing when disabled or offline", - "binding": "Binding", - "binding-type": { - "u": "U: Client is reachable via the UDP binding at any time.", - "m": "M: Client is reachable via the MQTT binding at any time.", - "h": "H: Client is reachable via the HTTP binding at any time.", - "t": "T: Client is reachable via the TCP binding at any time.", - "s": "S: Client is reachable via the SMS binding at any time.", - "n": "N: Client MUST send the response to such a request over the Non-IP binding (is supported since LWM2M 1.1).", - "uq": "UQ: UDP connection in queue mode (is not supported since LWM2M 1.1)", - "uqs": "UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", - "tq": "TQ: TCP connection in queue mode (is not supported since LWM2M 1.1)", - "tqs": "TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", - "sq": "SQ: SMS connection in queue mode (is not supported since LWM2M 1.1)" - }, - "binding-tooltip": "This is the list in the\"binding\" resource of the LwM2M server object - /1/x/7.\nIndicates the supported binding modes in the LwM2M Client.\nThis value SHOULD be the same as the value in the “Supported Binding and Modes” resource in the Device Object (/3/0/16).\nWhile multiple transports are supported, only one transport binding can be used during the entire Transport Session.\nAs an example, when UDP and SMS are both supported, the LwM2M Client and the LwM2M Server can choose to communicate either over UDP or SMS during the entire Transport Session.", - "bootstrap-server": "Bootstrap Server", - "lwm2m-server": "LwM2M Server", - "include-bootstrap-server": "Include Bootstrap Server updates", - "bootstrap-update-title": "You already have configured Bootstrap Server. Are you sure you want to exclude the updates?", - "bootstrap-update-text": "Be careful, after the confirmation the Bootstrap Server configuration data will become unrecoverable.", - "server-host": "Host", - "server-host-required": "Host is required.", - "server-port": "Port", - "server-port-required": "Port is required.", - "server-port-pattern": "Port must be a positive integer.", - "server-port-range": "Port should be in a range from 1 to 65535.", - "server-public-key": "Server Public Key", - "server-public-key-required": "Server Public Key is required.", - "client-hold-off-time": "Hold Off Time", - "client-hold-off-time-required": "Hold Off Time is required.", - "client-hold-off-time-pattern": "Hold Off Time must be a positive integer.", - "client-hold-off-time-tooltip": "Client Hold Off Time for use with a Bootstrap-Server only", - "account-after-timeout": "Account after the timeout", - "account-after-timeout-required": "Account after the timeout is required.", - "account-after-timeout-pattern": "Account after the timeout must be a positive integer.", - "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.", - "server-type": "Server type", - "add-new-server-title": "Add new server config", - "add-server-config": "Add server config", - "add-lwm2m-server-config": "Add LwM2M server", - "no-config-servers": "No servers configured", - "others-tab": "Other settings", - "client-strategy": "Client strategy when connecting", - "client-strategy-label": "Strategy", - "client-strategy-only-observe": "Only Observe Request to the client after the initial connection", - "client-strategy-read-all": "Read All Resources & Observe Request to the client after registration", - "fw-update": "Firmware update", - "fw-update-strategy": "Firmware update strategy", - "fw-update-strategy-data": "Push firmware update as binary file using Object 19 and Resource 0 (Data)", - "fw-update-strategy-package": "Push firmware update as binary file using Object 5 and Resource 0 (Package)", - "fw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push firmware update as Object 5 and Resource 1 (Package URI)", - "sw-update": "Software update", - "sw-update-strategy": "Software update strategy", - "sw-update-strategy-package": "Push binary file using Object 9 and Resource 2 (Package)", - "sw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push software update using Object 9 and Resource 3 (Package URI)", - "fw-update-resource": "Firmware update CoAP resource", - "fw-update-resource-required": "Firmware update CoAP resource is required.", - "sw-update-resource": "Software update CoAP resource", - "sw-update-resource-required": "Software update CoAP resource is required.", - "config-json-tab": "Json Config Profile Device", - "attributes-name": { - "min-period": "Minimum period", - "max-period": "Maximum period", - "greater-than": "Greater than", - "less-than": "Less than", - "step": "Step", - "min-evaluation-period": "Minimum evaluation period", - "max-evaluation-period": "Maximum evaluation period" - }, - "composite-operations-support": "Supports composite Read/Write/Observe operations" - }, - "snmp": { - "add-communication-config": "Add communication config", - "add-mapping": "Add mapping", - "authentication-passphrase": "Authentication passphrase", - "authentication-passphrase-required": "Authentication passphrase is required.", - "authentication-protocol": "Authentication protocol", - "authentication-protocol-required": "Authentication protocol is required.", - "communication-configs": "Communication configs", - "community": "Community string", - "community-required": "Community string is required.", - "context-name": "Context name", - "data-key": "Data key", - "data-key-required": "Data key is required.", - "data-type": "Data type", - "data-type-required": "Data type is required.", - "engine-id": "Engine ID", - "host": "Host", - "host-required": "Host is required.", - "oid": "OID", - "oid-pattern": "Invalid OID format.", - "oid-required": "OID is required.", - "please-add-communication-config": "Please add communication config", - "please-add-mapping-config": "Please add mapping config", - "port": "Port", - "port-format": "Invalid port format.", - "port-required": "Port is required.", - "privacy-passphrase": "Privacy passphrase", - "privacy-passphrase-required": "Privacy passphrase is required.", - "privacy-protocol": "Privacy protocol", - "privacy-protocol-required": "Privacy protocol is required.", - "protocol-version": "Protocol version", - "protocol-version-required": "Protocol version is required.", - "querying-frequency": "Querying frequency, ms", - "querying-frequency-invalid-format": "Querying frequency must be a positive integer.", - "querying-frequency-required": "Querying frequency is required.", - "retries": "Retries", - "retries-invalid-format": "Retries must be a positive integer.", - "retries-required": "Retries is required.", - "scope": "Scope", - "scope-required": "Scope is required.", - "security-name": "Security name", - "security-name-required": "Security name is required.", - "timeout-ms": "Timeout, ms", - "timeout-ms-invalid-format": "Timeout must be a positive integer.", - "timeout-ms-required": "Timeout is required.", - "user-name": "User name", - "user-name-required": "User name is required." - } + "color": { + "color-settings": "Spalvų nustatymai", + "color-type-constant": "Pastovi", + "color-type-gradient": "Gradientas", + "color-type-range": "Intervalas", + "color-type-function": "Funkcija", + "color": "Spalva", + "value-range": "Reikšmių intervalas", + "from": "Nuo", + "to": "Iki", + "color-function": "Spalvos funkcija", + "copy-color-settings-from": "Kopijuoti spalvų nustatymus iš", + "copy-from": "Kopijuoti iš", + "settings-type": "Nustatymų tipas", + "basic-mode": "Paprastas", + "advanced-mode": "Išplėstinis", + "entity-alias": "Objekto pseudonimas", + "entity-attribute": "Objekto atributas", + "gradient-color": "Gradiento spalva", + "gradient-color-min": "Spalva", + "gradient-start": "Gradiento pradžios spalva", + "gradient-start-min": "Pradžia", + "gradient-end": "Gradiento pabaigos spalva", + "gradient-end-min": "Pabaiga", + "start-value": "Pradinė reikšmė", + "end-value": "Galutinė reikšmė", + "gradient-type": "Gradiento tipas" + }, + "dashboard-state": { + "dashboard-state-settings": "Valdymo skydelio būsenos nustatymai", + "dashboard-state": "Valdymo skydelio būsenos ID", + "autofill-state-layout": "Automatiškai užpildyti būsenos išdėstymo aukštį pagal numatymą", + "default-margin": "Numatytasis valdiklių tarpas", + "default-background-color": "Numatytoji fono spalva", + "sync-parent-state-params": "Sinchronizuoti būsenos parametrus su pagrindiniu valdymo skydeliu" + }, + "date-range-navigator": { + "date-range-picker-settings": "Datų intervalo parinkiklio nustatymai", + "hide-date-range-picker": "Slėpti datų intervalo parinkiklį", + "picker-one-panel": "Datų intervalo parinkiklis viename skydelyje", + "picker-auto-confirm": "Automatinis datų intervalo patvirtinimas", + "picker-show-template": "Rodyti datų intervalo šabloną", + "first-day-of-week": "Pirmoji savaitės diena", + "interval-settings": "Intervalo nustatymai", + "hide-interval": "Slėpti intervalą", + "initial-interval": "Pradinis intervalas", + "interval-hour": "Valanda", + "interval-day": "Diena", + "interval-week": "Savaitė", + "interval-two-weeks": "2 savaitės", + "interval-month": "Mėnuo", + "interval-three-months": "3 mėnesiai", + "interval-six-months": "6 mėnesiai", + "step-settings": "Žingsnio nustatymai", + "hide-step-size": "Slėpti žingsnio dydį", + "initial-step-size": "Pradinis žingsnio dydis", + "hide-labels": "Slėpti etiketes", + "use-session-storage": "Naudoti sesijos saugyklą", + "localizationMap": { + "Sun": "Sek", + "Mon": "Pir", + "Tue": "Ant", + "Wed": "Tre", + "Thu": "Ket", + "Fri": "Pen", + "Sat": "Šeš", + "Jan": "Sau", + "Feb": "Vas", + "Mar": "Kov", + "Apr": "Bal", + "May": "Geg", + "Jun": "Bir", + "Jul": "Lie", + "Aug": "Rgp", + "Sep": "Rgs", + "Oct": "Spa", + "Nov": "Lap", + "Dec": "Grd", + "January": "Sausis", + "February": "Vasaris", + "March": "Kovas", + "April": "Balandis", + "June": "Birželis", + "July": "Liepa", + "August": "Rugpjūtis", + "September": "Rugsėjis", + "October": "Spalis", + "November": "Lapkritis", + "December": "Gruodis", + "Custom Date Range": "Pasirinktinis datų intervalas", + "Date Range Template": "Datų intervalo šablonas", + "Today": "Šiandien", + "Yesterday": "Vakar", + "This Week": "Šią savaitę", + "Last Week": "Praėjusią savaitę", + "This Month": "Šį mėnesį", + "Last Month": "Praėjusį mėnesį", + "Year": "Metai", + "This Year": "Šiais metais", + "Last Year": "Praėjusiais metais", + "Date picker": "Datų parinkiklis", + "Hour": "Valanda", + "Day": "Diena", + "Week": "Savaitė", + "2 weeks": "2 savaitės", + "Month": "Mėnuo", + "3 months": "3 mėnesiai", + "6 months": "6 mėnesiai", + "Custom interval": "Pasirinktinis intervalas", + "Interval": "Intervalas", + "Step size": "Žingsnio dydis", + "Ok": "Gerai" + } }, - "dialog": { - "close": "Uždaryti dialogo langą", - "error-message-title": "Klaidos pranešimas:", - "error-details-title": "Informacija apie klaidą" + "doughnut": { + "doughnut-appearance": "Žiedo diagramos išvaizda", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-with-total": "Su suma", + "central-total-value": "Centrinė bendra reikšmė", + "doughnut-card-style": "Žiedo diagramos kortelės stilius" }, - "direction": { - "column": "Stulpelis", - "row": "Eilutė" + "entities-hierarchy": { + "hierarchy-data-settings": "Hierarchijos duomenų nustatymai", + "relations-query-function": "Mazgo ryšių užklausos funkcija", + "has-children-function": "Funkcija, tikrinanti, ar mazgas turi vaikų", + "node-state-settings": "Mazgo būsenos nustatymai", + "node-opened-function": "Numatytoji mazgo atidarymo funkcija", + "node-disabled-function": "Mazgo išjungimo funkcija", + "display-settings": "Atvaizdavimo nustatymai", + "node-icon-function": "Mazgo piktogramos funkcija", + "node-text-function": "Mazgo teksto funkcija", + "sort-settings": "Rikiavimo nustatymai", + "nodes-sort-function": "Mazgų rikiavimo funkcija" }, "edge": { - "all": "All", - "all-edges": "All edges", - "groups": "Groups", - "shared": "Shared", - "edge": "Edge", - "edge-instances": "Edge instances", - "instances": "Instances", - "edge-file": "Edge file", - "name-max-length": "Name should be less than 256", - "label-max-length": "Label should be less than 256", - "type-max-length": "Type should be less than 256", - "management": "Edge management", - "no-edges-matching": "No edges matching '{{entity}}' were found.", - "add": "Add Edge", - "no-edges-text": "No edges found", - "edge-details": "Edge details", - "add-edge-text": "Add new edge", - "delete": "Delete edge", - "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", - "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", - "delete-edges-title": "Are you sure you want to edge { count, plural, =1 {1 edge} other {# edges} }?", - "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", - "name": "Name", - "name-starts-with": "Edge name starts with", - "name-required": "Name is required.", - "edge-license-key": "Edge License Key", - "edge-license-key-required": "Edge License Key is required.", - "edge-license-key-max-length": "Edge License Key should be less than 31", - "edge-license-key-hint": "IMPORTANT NOTE: default edge license key provided only for evaluation purposes.
    This key is active only for 30 days after the activation. White-labeling feature is disabled.
    To obtain your license key please navigate to the pricing page and select the best license option for your case.", - "cloud-endpoint": "Cloud Endpoint", - "cloud-endpoint-required": "Cloud Endpoint is required.", - "cloud-endpoint-max-length": "Cloud Endpoint should be less than 256", - "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", - "description": "Description", - "details": "Details", - "events": "Events", - "copy-id": "Copy Edge Id", - "id-copied-message": "Edge Id has been copied to clipboard", - "sync": "Sync Edge", - "edge-required": "Edge required", - "edge-type": "Edge type", - "edge-type-required": "Edge type is required.", - "event-action": "Event action", - "entity-id": "Entity ID", - "select-edge-type": "Select edge type", - "assign-to-customer": "Assign to customer", - "assign-to-customer-text": "Please select the customer to assign the edge(s)", - "assign-edge-to-customer": "Assign Edge(s) To Customer", - "assign-edge-to-customer-text": "Please select the edges to assign to the customer", - "assignedToCustomer": "Assigned to customer", - "edge-public": "Edge is public", - "assigned-to-customer": "Assigned to: {{customerTitle}}", - "unassign-from-customer": "Unassign from customer", - "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", - "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", - "unassign-edges-title": "Are you sure you want to unassign { count, plural, =1 {1 edge} other {# edges} }?", - "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", - "make-public": "Make edge public", - "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", - "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", - "make-private": "Make edge private", - "public": "Public", - "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", - "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", - "import": "Import edge", - "install-connect-instructions": "Install & Connect Instructions", - "install-connect-instructions-edge-created": "Edge created! Check Install & Connect Instructions", - "loading-edge-instructions": "Loading edge instructions...", - "label": "Label", - "load-entity-error": "Failed to load data. Entity has been deleted.", - "assign-new-edge": "Assign new edge", - "unassign-from-edge": "Unassign from edge", - "edge-key": "Edge key", - "copy-edge-key": "Copy Edge key", - "edge-key-copied-message": "Edge key has been copied to clipboard", - "edge-secret": "Edge secret", - "copy-edge-secret": "Copy Edge secret", - "edge-secret-copied-message": "Edge secret has been copied to clipboard", - "manage-assets": "Manage assets", - "manage-devices": "Manage devices", - "manage-entity-views": "Manage entity views", - "manage-dashboards": "Manage dashboards", - "manage-rulechains": "Manage rule chains", - "assets": "Edge assets", - "devices": "Edge devices", - "entity-views": "Edge entity views", - "dashboard": "Edge dashboard", - "dashboards": "Edge Dashboards", - "rulechain-templates": "Rule chain templates", - "edge-rulechain-templates": "Edge rule chain templates", - "converter-templates": "Converter templates", - "edge-converter-templates": "Edge converter templates", - "integration-templates": "Integration templates", - "edge-integration-templates": "Edge integration templates", - "rulechains": "Edge rule chains", - "integrations": "Integrations", - "search": "Search edges", - "selected-edges": "{ count, plural, =1 {1 edge} other {# edges} } selected", - "any-edge": "Any edge", - "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", - "edge-type-list-empty": "No edge types selected.", - "edge-types": "Edge types", - "enter-edge-type": "Enter edge type", - "deployed": "Deployed", - "pending": "Pending", - "downlinks": "Downlinks", - "no-downlinks-prompt": "No downlinks found", - "sync-process-started-successfully": "Sync process started successfully!", - "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", - "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

    List of missing rule chain(s):
    {{missingRuleChains}}", - "widget-datasource-error": "This widget supports only EDGE entity datasource", - "assign-to-edge": "Assign to edge", - "assign-to-edge-title": "Assign Entity Group(s) to Edge", - "manage-edge-user-groups": "Manage edge user groups", - "manage-edge-asset-groups": "Manage edge asset groups", - "manage-edge-device-groups": "Manage edge device groups", - "manage-edge-entity-view-groups": "Manage edge entity view groups", - "manage-edge-dashboard-groups": "Manage edge dashboard groups", - "manage-edge-rule-chains": "Manage edge rule chains", - "manage-edge-integrations": "Manage edge integrations", - "manage-edge-scheduler-events": "Manage edge scheduler events", - "select-group-to-add": "Select target group to add selected edges", - "select-group-to-move": "Select target group to move selected edges", - "remove-edges-from-group": "Are you sure you want to remove { count, plural, =1 {1 edge} other {# edges} } from group '{entityGroup}'?", - "group": "Group of edges", - "list-of-groups": "{ count, plural, =1 {One edge group} other {List of # edge groups} }", - "group-name-starts-with": "Edge groups whose names start with '{{prefix}}'", - "unassign-entity-group-from-edge-title": "Are you sure you want to unassign the entity group '{{ entityGroupName }}'?", - "unassign-entity-group-from-edge-text": "After the confirmation the entity group will be unassigned and won't be accessible by the edge.", - "unassign-entity-group-from-edge": "Unassign entity group from edge", - "unassign-entity-groups-from-edge-title": "Are you sure you want to unassign {{count}} entity groups?", - "unassign-entity-groups-from-edge-text": "After the confirmation entity groups will be unassigned and won't be accessible by the edge.", - "unassign-entity-groups-from-edge": "Unassign entity groups from edge", - "unassign-scheduler-events-from-edge": "Unassign scheduler events from edge", - "unassign-scheduler-event-from-edge-title": "Are you sure you want to unassign scheduler event '{{schedulerEventName}}'?", - "unassign-scheduler-event-from-edge-text": "After the confirmation the scheduler event will be unassigned and won't be accessible by the edge.", - "unassign-scheduler-events-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 scheduler event} other {# scheduler events} }?", - "unassign-scheduler-events-from-edge-text": "After the confirmation all selected scheduler events will be unassigned and won't be accessible by the edge.", - "manage-user-groups": "Manage user groups", - "manage-asset-groups": "Manage asset groups", - "manage-device-groups": "Manage device groups", - "manage-dashboard-groups": "Manage dashboard groups", - "manage-entity-view-groups": "Manage entity view groups", - "manage-scheduler-events": "Manage scheduler events", - "manage-integrations": "Manage integrations", - "assign-scheduler-event-to-edge-title": "Assign Scheduler Events To Edge", - "assign-scheduler-event-to-edge-text": "Please select the scheduler events to assign to the edge", - "assign-entity-groups-to-edge-text": "Please select the entity groups to assign to the edge", - "assign-integration-to-edge-title": "Assign Integrations To Edge", - "assign-integration-to-edge-text": "Please select the integrations to assign to the edge", - "missing-attributes-title": "Edge has missing attribute(s)", - "missing-attributes-text": "Integration(s), assigned to edge, have attribute placeholders that are not available on this edge.

    List of missing edge attributes(s):
    {{missingEdgeAttributes}}", - "missing-all-related-attributes-title": "Edge(s) have missing attribute(s)", - "missing-all-related-attributes-text": "Edge(s), related to integration, don't have attributes that are present inside integration configuration as attribute placeholders.

    List of missing edge attributes(s):
    {{missingEdgeAttributes}}", - "quick-overview-widget-header": "{{edgeName}} Quick Overview" - }, - "edge-event": { - "type-dashboard": "Dashboard", - "type-asset": "Asset", - "type-device": "Device", - "type-device-profile": "Device Profile", - "type-asset-profile": "Asset Profile", - "type-entity-view": "Entity View", - "type-alarm": "Alarm", - "type-entity-group": "Entity Group", - "type-rule-chain": "Rule Chain", - "type-rule-chain-metadata": "Rule Chain Metadata", - "type-edge": "Edge", - "type-user": "User", - "type-tenant": "Tenant", - "type-tenant-profile": "Tenant Profile", - "type-customer": "Customer", - "type-relation": "Relation", - "type-widgets-bundle": "Widgets Bundle", - "type-widgets-type": "Widgets Type", - "type-admin-settings": "Admin Settings", - "type-ota-package": "Ota Package", - "type-queue": "Queue", - "type-scheduler-event": "Scheduler Event", - "type-white-labeling": "White Labeling", - "type-login-white-labeling": "Login White Labeling", - "type-custom-translation": "Custom Translation", - "type-role": "Role", - "type-group-permission": "Group Permission", - "type-integration": "Integration", - "type-converter": "Converter", - "action-type-added": "Added", - "action-type-deleted": "Deleted", - "action-type-updated": "Updated", - "action-type-post-attributes": "Post Attributes", - "action-type-attributes-updated": "Attributes Updated", - "action-type-attributes-deleted": "Attributes Deleted", - "action-type-timeseries-updated": "Timeseries Updated", - "action-type-credentials-updated": "Credentials Updated", - "action-type-assigned-to-customer": "Assigned to Customer", - "action-type-unassigned-from-customer": "Unassigned from Customer", - "action-type-relation-add-or-update": "Relation Add or Update", - "action-type-relation-deleted": "Relation Deleted", - "action-type-rpc-call": "RPC Call", - "action-type-alarm-ack": "Alarm Ack", - "action-type-alarm-clear": "Alarm Clear", - "action-type-alarm-assigned": "Alarm Assigned", - "action-type-alarm-unassigned": "Alarm Unassigned", - "action-type-assigned-to-edge": "Assigned to Edge", - "action-type-unassigned-from-edge": "Unassigned from Edge", - "action-type-credentials-request": "Credentials Request", - "action-type-entity-merge-request": "Entity Merge Request", - "action-type-added-to-entity-group": "Added to Entity Group", - "action-type-removed-from-entity-group": "Removed from Entity Group", - "action-type-change-owner": "Change owner" + "display-default-title": "Rodyti numatytąjį pavadinimą" }, - "error": { - "unable-to-connect": "Nepavyksta prisijungti prie serverio! Patikrinkite interneto ryšį.", - "unhandled-error-code": "Neapdorotos klaidos kodas: {{errorCode}}", - "unknown-error": "Nežinoma klaida" - }, - "entity": { - "entity": "Subjektas", - "entities": "Subjektai", - "entities-count": "Subjektų kiekis", - "alarms-count": "Įspėjimų kiekis", - "aliases": "Subjektų pseudonimai", - "aliases-short": "Pseudonimai", - "entity-alias": "Subjekto pseudonimas", - "unable-delete-entity-alias-title": "Subjekto pseudonimo panaikinti nepavyko", - "unable-delete-entity-alias-text": "Subjekto pseudonimas '{{entityAlias}}' negali būti panaikintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", - "duplicate-alias-error": "Pseudonimas '{{alias}}' dubliuojasi.
    Subjektų pseudonimai skydelyje negali kartotis.", - "missing-entity-filter-error": "Trūksta pseudonimo '{{alias}}' filtro.", - "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", - "alias": "Pseudonimas", - "alias-required": "Subjekto pseudonimas būtinas.", - "remove-alias": "Panaikinti subjekto pseudonimą", - "add-alias": "Pridėti subjekto pseudonimą", - "entity-list": "Subjektų sąrašas", - "entity-type": "Subjekto tipas", - "entity-types": "Subjektų tipai", - "entity-type-list": "Subjektų tipų sąrašas", - "any-entity": "Bet kuris subjektas", - "add-entity-type": "Pridėti subjekto tipą", - "enter-entity-type": "Įveskitesubjekto tipą", - "no-entities-matching": "Subjektų, atitinkančių '{{entity}}' nėra.", - "no-entity-types-matching": "Subjektų tipų, atitinkančių '{{entityType}}' nėra.", - "name-starts-with": "Pavadinimas prasideda", - "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%subjekto_pavadinimo_fragmentas%', '%subjekto_pavadinimo_pabaiga', 'subjekto_pavadinimo_pradžia%'.", - "use-entity-name-filter": "Naudoti filtrą", - "entity-list-empty": "Subjektai nepasirinkti.", - "entity-type-list-required": "Nors vienas subjekto tipas turi būti pasirinktas.", - "entity-name-filter-required": "Subjekto vardo filtras būtinas.", - "entity-name-filter-no-entity-matched": "Subjektų, kurių pavadinimas prasideda '{{entity}}' nėra.", - "all-subtypes": "Visi", - "select-entities": "Pasirinkte subjektus", - "no-aliases-found": "Pseudonimų nėra.", - "no-alias-matching": "'{{alias}}' nėra.", - "create-new-alias": "Sukurti naują!", - "create-new": "Sukurti naują", - "key": "Raktas", - "key-name": "Rakto pavadinimas", - "no-keys-found": "Rakto nėra.", - "no-key-matching": "'{{key}}' nėra.", - "create-new-key": "Sukurti naują!", - "type": "Tipas", - "type-required": "Subjekto tipas būtinas.", - "type-device": "Įrenginys", - "type-devices": "Įrenginiai", - "list-of-devices": "{ count, plural, =1 {Vienas įrenginys} other {# įrenginių sąrašas} }", - "device-name-starts-with": "Įrenginiai, kurių pavadinimas prasideda '{{prefix}}'", - "type-device-profile": "Įrenginio profilis", - "type-device-profiles": "Device profiles", - "clear-selected-profiles": "Clear selected profiles", - "list-of-device-profiles": "{ count, plural, =1 {One device profile} other {List of # device profiles} }", - "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", - "type-asset-profile": "Asset profile", - "type-asset-profiles": "Asset profiles", - "list-of-asset-profiles": "{ count, plural, =1 {One asset profile} other {List of # asset profiles} }", - "asset-profile-name-starts-with": "Asset profiles whose names start with '{{prefix}}'", - "type-asset": "Turtas", - "type-assets": "Turtas", - "list-of-assets": "{ count, plural, =1 {Turtas} other {# Turto sąrašas} }", - "asset-name-starts-with": "Turtas, kurio pavadinimas prasideda '{{prefix}}'", - "type-entity-view": "Subjekto rodinys", - "type-entity-views": "Subjektų rodinys", - "list-of-entity-views": "{ count, plural, =1 {Vienas subjekto rodinys} other {# Subjektų rodinių sąrašas} }", - "entity-view-name-starts-with": "Subjektų rodiniai, kurių pavadinimai prasideda '{{prefix}}'", - "type-rule": "Taisyklė", - "type-rules": "Taisyklės", - "list-of-rules": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", - "rule-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", - "type-plugin": "Papildinys", - "type-plugins": "Papildiniai", - "list-of-plugins": "{ count, plural, =1 {Vienas papildinys} other {# Papildinių sąrašas} }", - "plugin-name-starts-with": "Papildiniai, kurių pavadinimai prasideda '{{prefix}}'", - "type-tenant": "Valdytojas", - "type-tenants": "valdytojai", - "list-of-tenants": "{ count, plural, =1 {Vienas valdytojas} other {# Valdytojų sąrašas} }", - "tenant-name-starts-with": "Valdytojai, kurių pavadinimai prasideda '{{prefix}}'", - "type-tenant-profile": "Tenant profile", - "type-tenant-profiles": "Tenant profiles", - "list-of-tenant-profiles": "{ count, plural, =1 {One tenant profile} other {List of # tenant profiles} }", - "tenant-profile-name-starts-with": "Tenant profiles whose names start with '{{prefix}}'", - "type-customer": "Klientas", - "type-customers": "Klientai", - "list-of-customers": "{ count, plural, =1 {Vienas klientas} other {# Klientų sąrašas} }", - "customer-name-starts-with": "Klientai, kurių pavadinimai prasideda '{{prefix}}'", - "type-user": "Vartotojas", - "type-users": "Vartotojai", - "list-of-users": "{ count, plural, =1 {Vienas vartotojas} other {# Vartotojų sąrašas} }", - "user-name-starts-with": "Vartotojai, kurių pavadinimai prasideda '{{prefix}}'", - "type-dashboard": "Skydelis", - "type-dashboards": "Skydeliai", - "list-of-dashboards": "{ count, plural, =1 {Vienas skydelis} other {# Skydelių sąrašas} }", - "dashboard-name-starts-with": "Skydeliai, kurių pavadinimai prasideda '{{prefix}}'", - "type-alarm": "Įspėjimas", - "type-alarms": "Įspėjimai", - "list-of-alarms": "{ count, plural, =1 {Vienas įspėjimas} other {# Įspėjimų sąrašas} }", - "alarm-name-starts-with": "Įspėjimai, kurių pavadinimai prasideda '{{prefix}}'", - "type-rulechain": "Taisyklių grandinė", - "type-rulechains": "Taisyklių grandinės", - "list-of-rulechains": "{ count, plural, =1 {Viena taisyklių grandinė} other {# Taisyklių grandinių sąrašas} }", - "rulechain-name-starts-with": "Taisyklių grandinės, kurų pavadinimai prasideda '{{prefix}}'", - "type-scheduler-event": "Tvarkaraščio įvykis", - "type-scheduler-events": "Tvarkaraščio įvykiai", - "list-of-scheduler-events": "{ count, plural, =1 {Vienas tvarkaraščio įvykis} other {# Tvarkaraščio įvykių sąrašas} }", - "scheduler-event-name-starts-with": "Tvarkaraščio įvykiai, kurių pavadinimai prasideda '{{prefix}}'", - "type-blob-entity": "Blob entity", - "type-blob-entities": "Blob entities", - "list-of-blob-entities": "{ count, plural, =1 {One blob entity} other {List of # blob entities} }", - "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", - "type-rulenode": "Tasyklė", - "type-rulenodes": "Taisyklės", - "list-of-rulenodes": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", - "rulenode-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", - "type-current-customer": "Dabartinis klientas", - "type-current-tenant": "Dabartinis valdytojas", - "type-current-user": "Dabartinis vartotojas", - "type-current-user-owner": "Dabartinio vartotojo savininkas", - "type-widgets-bundle": "Valdiklių rinkinys", - "type-widgets-bundles": "Valdiklių rinkiniai", - "list-of-widgets-bundles": "{ count, plural, =1 {Vienas valdiklių rinkinys} other {# Valdiklių rinkinių sąrašas} }", - "type-widget-type": "Valdiklio tipas", - "type-widget-types": "Valdiklių tipai", - "list-of-widget-types": "{ count, plural, =1 {Vienas valdiklio tipas} other {# Valdiklių tipų sąrašas} }", - "search": "Subjektų paieška", - "selected-entities": "Pasirinkta { count, plural, =1 {1 subjektas} other {# subjektai} }", - "entity-name": "Subjekto pavadinimas", - "entity-label": "Subjekto etikelė", - "details": "Informacija apie subjektą", - "no-entities-prompt": "Subjektų nėra", - "no-data": "Duomenų atvaizdavimui nėra", - "columns-to-display": "Rodomi stulpeliai", - "type-api-usage-state": "Api Usage State", - "type-entity-group": "Subjektų grupė", - "type-converter": "Duomenų keitiklis", - "type-converters": "Duomenų keitikliai", - "list-of-converters": "{ count, plural, =1 {Vienas duomenų keitiklis} other {# Duomenų keitiklių sąrašas} }", - "converter-name-starts-with": "Duomenų keitikliai, kurių pavadinimai prasideda '{{prefix}}'", - "type-integration": "Integracija", - "type-integrations": "Integracijos", - "list-of-integrations": "{ count, plural, =1 {Viena integracija} other {# Integracijų sąrašas} }", - "integration-name-starts-with": "Integracijos, kurių pavadinimai prasideda '{{prefix}}'", - "type-role": "Rolė", - "type-roles": "Rolės", - "list-of-roles": "{ count, plural, =1 {Viena rolė} other {# rolių sąrašas} }", - "role-name-starts-with": "Rolės, kurių pavadinimai prasideda '{{prefix}}'", - "type-group-permission": "Grupės leidimai", - "type-edge": "Edge", - "type-edges": "Edges", - "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", - "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", - "type-tb-resource": "Resource", - "type-ota-package": "OTA package", - "type-rpc": "RPC", - "type-queue": "Queue", - "type-notification": "Notification", - "type-notification-rule": "Notification rule", - "type-notification-rules": "Notification rules", - "list-of-notification-rules": "{ count, plural, =1 {One notification rule} other {List of # notification rules} }", - "type-notification-target": "Notification recipient", - "type-notification-targets": "Notification recipients", - "list-of-notification-targets": "{ count, plural, =1 {One notification recipient} other {List of # notification recipients} }", - "type-notification-request": "Notification request", - "type-notification-template": "Notification template", - "type-notification-templates": "Notification templates", - "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", - "customer-name": "Klientas", - "sub-customer-name": "Priklausantis klientas", - "parent-customer-name": "Pirminis klientas", - "parent-sub-customer-name": "Pirminis klientas", - "include-customer-entities": "Rodyti klientui priklausančius klientus", - "include-sub-customer-entities": "Rodyti priklausančių klientų subjektus", - "groups": "Grupė" - }, - "entity-group": { - "entity-group": "Subjektų grupė", - "details": "Informacija", - "columns": "Stulpeliai", - "add-column": "Pridėti stulpelį", - "column-value": "Reikšmė", - "column-value-required": "Stulpelio reikšmė būtina.", - "column-title": "Pavadinimas", - "default-sort-order": "Numatytoji rūšiavimo tvarka", - "default-sort-order-required": "Numatytoji rūšiavimo tvarka būtina.", - "hide-in-mobile-view": "Nerodyti mobiliąjame režime", - "use-cell-style-function": "Taikyti langelio stiliaus funkciją", - "use-cell-content-function": "Taikyti langelio turinio funkciją", - "edit-column": "Redaguoti stulpelį", - "column-details": "Stulpelio informacija", - "actions": "Veiksmai", - "settings": "Nustatymai", - "search": "Subjektų grupių paieška", - "delete": "Panaikinti subjektų grupę", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", - "description": "Aprašymas", - "add": "Pridėti subjektų grupę", - "open-entity-group": "Atverti subjektų grupę", - "add-entity-group-text": "Pridėti naują subjektų grupę", - "no-entity-groups-text": "Subjektų grupių nėra", - "entity-group-details": "Informacija apie subjektų grupę", - "selected-entity-groups": "Pasirinkta { count, plural, =1 {1 subjektų grupė} other {# subjektų grupės} }", - "delete-entity-groups": "Panaikinti subjektų grupes", - "delete-entity-group-title": "Ar tikrai norite panaikinti subjektų grupę '{{entityGroupName}}'?", - "delete-entity-group-text": "Būkite dėmesingi, po patvirtinimo, subjektų grupė ir visa su ja susijusi informacija bus pašalinta ir jų atkurti nebegalėsite.", - "delete-entity-groups-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }?", - "delete-entity-groups-action-title": "Panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }", - "delete-entity-groups-text": "Būkite dėmesingi, po patvirtinimo visos subjektų grupės ir su jomis susijusi informacija bus panaikintos ir jų atkurti nebegalėsite.", - "device-groups": "Įrenginių grupės", - "shared-device-groups": "Bendrinamos įrenginių grupės", - "asset-groups": "Turto grupės", - "shared-asset-groups": "Bendrinamos turto grupės", - "customer-groups": "Klientų grupės", - "shared-customer-groups": "Bendrinamos klientų grupės", - "device-group": "Įrenginių grupė", - "asset-group": "Turto grupė", - "user-group": "Vartotojų grupė", - "user-groups": "Vartotojų grupės", - "customer-group": "Klientų grupė", - "entity-view-groups": "Subjektų rodinių grupė", - "shared-entity-view-groups": "Bendrinamos subjektų rodinių grupės", - "entity-view-group": "Subjektų rodinių grupė", - "edge-groups": "Edge groups", - "shared-edge-groups": "Shared edge groups", - "edge-group": "Edge group", - "dashboard-groups": "Skydelių grupės", - "shared-dashboard-groups": "Bendrinamos skydelių grupės", - "dashboard-group": "Skydelių grupė", - "fetch-more": "Gauti daugiau", - "column-type": { - "column-type": "Stulpelio tipas", - "client-attribute": "Kliento atributas", - "shared-attribute": "Bendrinamas atributas", - "server-attribute": "Serverio atributas", - "timeseries": "Telemetrija", - "entity-field": "Subjekto laukas" + "gateway": { + "general-settings": "Bendrieji nustatymai", + "widget-title": "Valdiklio pavadinimas", + "default-archive-file-name": "Numatytasis archyvo failo pavadinimas", + "device-type-for-new-gateway": "Įrenginio tipas naujam tinklo šliuzui", + "messages-settings": "Pranešimų nustatymai", + "save-config-success-message": "Tekstas, rodomas sėkmingai išsaugojus šliuzo konfigūraciją", + "device-name-exists-message": "Tekstas, rodomas kai įrenginys su įvestu pavadinimu jau egzistuoja", + "gateway-title": "Šliuzo forma", + "read-only": "Tik skaitymui", + "events-title": "Šliuzo įvykių formos pavadinimas", + "events-filter": "Įvykių filtras", + "event-key-contains": "Įvykio rakte yra...", + "show-connector": "Rodyti jungtį", + "connector-state-param-key": "Jungties būsenos parametro raktas", + "message": "Pranešimas", + "level": "Lygis", + "created-time": "Sukūrimo laikas" + }, + "gauge": { + "default-color": "Numatytoji spalva", + "radial-gauge-settings": "Radialinio matuoklio nustatymai", + "ticks-settings": "Padalų nustatymai", + "min-value": "Minimali reikšmė", + "max-value": "Maksimali reikšmė", + "min-value-short": "min", + "max-value-short": "maks", + "start-ticks-angle": "Pradinis padalų kampas", + "ticks-angle": "Padalų kampas", + "major-ticks": "Pagrindinės padalos", + "major-ticks-count": "Pagrindinių padalų skaičius", + "major-ticks-color": "Pagrindinių padalų spalva", + "minor-ticks": "Antrinės padalos", + "minor-ticks-count": "Antrinių padalų skaičius", + "minor-ticks-color": "Antrinių padalų spalva", + "tick-numbers-font": "Padalų numerių šriftas", + "unit-title-settings": "Matavimo vieneto pavadinimo nustatymai", + "show-unit-title": "Rodyti vieneto pavadinimą", + "unit-title": "Vieneto pavadinimas", + "title-font": "Pavadinimo teksto šriftas", + "units-settings": "Vienetų nustatymai", + "units-font": "Vienetų teksto šriftas", + "value-box-settings": "Reikšmės laukelio nustatymai", + "show-value-box": "Rodyti reikšmės laukelį", + "value-box": "Reikšmės laukelis", + "value-int": "Skaitmenų skaičius sveikai reikšmės daliai", + "value-text": "Reikšmės tekstas", + "value-text-shadow": "Reikšmės teksto šešėlis", + "value-font": "Reikšmės teksto šriftas", + "rect-stroke-color-start": "Stačiakampio rėmelio spalva – gradiento pradžia", + "rect-stroke-color-end": "Stačiakampio rėmelio spalva – gradiento pabaiga", + "background-color": "Fono spalva", + "shadow-color": "Šešėlio spalva", + "value-box-rect-stroke-color": "Reikšmės laukelio rėmelio spalva", + "value-box-rect-stroke-color-end": "Reikšmės laukelio rėmelio spalva – gradiento pabaiga", + "value-box-background-color": "Reikšmės laukelio fono spalva", + "value-box-shadow-color": "Reikšmės laukelio šešėlio spalva", + "plate-settings": "Plokštės nustatymai", + "show-plate-border": "Rodyti plokštės rėmelį", + "plate-color": "Plokštės spalva", + "needle-settings": "Rodyklės nustatymai", + "needle-circle-size": "Rodyklės apskritimo dydis", + "needle-color": "Rodyklės spalva", + "needle-color-start": "Rodyklės spalva – gradiento pradžia", + "needle-color-end": "Rodyklės spalva – gradiento pabaiga", + "needle-color-shadow-up": "Viršutinės rodyklės dalies šešėlio spalva", + "needle-color-shadow-down": "Rodyklės metamo šešėlio spalva", + "highlights-settings": "Paryškinimų nustatymai", + "highlights-width": "Paryškinimų plotis", + "highlights": "Paryškinimai", + "highlight-from": "Nuo", + "highlight-to": "Iki", + "highlight-color": "Spalva", + "no-highlights": "Paryškinimai nesukonfigūruoti", + "add-highlight": "Pridėti paryškinimą", + "animation-settings": "Animacijos nustatymai", + "enable-animation": "Įjungti animaciją", + "animation-duration-rule": "Animacijos trukmės ir taisyklių nustatymas", + "animation-duration": "Animacijos trukmė", + "animation-rule": "Animacijos taisyklė", + "animation-linear": "Tiesinė", + "animation-quad": "Kvadratinė", + "animation-quint": "Penktos eilės", + "animation-cycle": "Ciklinė", + "animation-bounce": "Šokčiojanti", + "animation-elastic": "Elastinga", + "animation-dequad": "Atvirkštinė kvadratinė", + "animation-dequint": "Atvirkštinė penktos eilės", + "animation-decycle": "Atvirkštinis ciklas", + "animation-debounce": "Atšokimo mažinimas", + "animation-delastic": "Atvirkštinė elastinga", + "linear-gauge-settings": "Tiesinio matuoklio nustatymai", + "bar-stroke": "Juostos kontūras", + "bar-stroke-width": "Juostos kontūro plotis", + "bar-stroke-color": "Juostos kontūro spalva", + "bar-background-color": "Matuoklio juostos fono spalva", + "bar-background-color-end": "Juostos fono spalva – gradiento pabaiga", + "progress-bar-color": "Progreso juostos spalva", + "progress-bar": "Progreso juosta", + "progress-bar-color-start": "Progreso juostos spalva – gradiento pradžia", + "progress-bar-color-end": "Progreso juostos spalva – gradiento pabaiga", + "major-ticks-names": "Pagrindinių padalų pavadinimai", + "show-stroke-ticks": "Rodyti padalų kontūrus", + "major-ticks-font": "Pagrindinių padalų šriftas", + "border-color": "Rėmelio spalva", + "border-width": "Rėmelio plotis", + "needle-circle": "Rodyklės apskritimas", + "needle-circle-color": "Rodyklės apskritimo spalva", + "animation-target": "Animacijos objektas", + "animation-target-needle": "Rodyklė", + "animation-target-plate": "Plokštė", + "common-settings": "Bendrieji matuoklio nustatymai", + "gauge-type": "Matuoklio tipas", + "gauge-type-arc": "Lankas", + "gauge-type-donut": "Žiedas", + "gauge-type-horizontal-bar": "Horizontali juosta", + "gauge-type-vertical-bar": "Vertikali juosta", + "donut-start-angle": "Pradinis kampas", + "bar-settings": "Matuoklio juostos nustatymai", + "relative-bar-width": "Santykinis juostos plotis", + "neon-glow-brightness": "Neoninio švytėjimo ryškumas (0–100), 0 – išjungta", + "neon-glow-brightness-hint": "0 – išjungia efektą", + "stripes-thickness": "Juostelių storis, 0 – be juostelių", + "stripes-thickness-hint": "0 – be juostelių", + "rounded-line-cap": "Rodyti apvalintą linijos galą", + "bar-color-settings": "Juostos spalvų nustatymai", + "use-precise-level-color-values": "Naudoti tikslias spalvų reikšmes", + "bar-colors": "Juostos spalvos nuo žemiausios iki aukščiausios", + "color": "Spalva", + "no-bar-colors": "Juostos spalvos nesukonfigūruotos", + "add-bar-color": "Pridėti juostos spalvą", + "from": "Nuo", + "to": "Iki", + "fixed-level-colors": "Juostos spalvos pagal ribines reikšmes", + "gauge-title-settings": "Matuoklio pavadinimo nustatymai", + "show-gauge-title": "Rodyti matuoklio pavadinimą", + "gauge-title": "Matuoklio pavadinimas", + "gauge-title-font": "Matuoklio pavadinimo šriftas", + "unit-title-and-timestamp-settings": "Vieneto pavadinimo ir laiko žymos nustatymai", + "show-timestamp": "Rodyti reikšmės laiko žymą", + "timestamp-format": "Laiko žymos formatas", + "label-font": "Etiketės šriftas po reikšme", + "value-settings": "Reikšmės nustatymai", + "show-value": "Rodyti reikšmės tekstą", + "min-max-settings": "Minimalių/maksimalių reikšmių etikečių nustatymai", + "show-min-max": "Rodyti minimalias ir maksimalias reikšmes", + "min-max-font": "Minimalių ir maksimalių reikšmių šriftas", + "show-ticks": "Rodyti padalas", + "tick-width": "Padalos plotis", + "tick-color": "Padalos spalva", + "tick-values": "Padalų reikšmės", + "no-tick-values": "Padalų reikšmės nesukonfigūruotos", + "add-tick-value": "Pridėti padalos reikšmę", + "gauge-appearance": "Matuoklio išvaizda", + "units-title": "Vienetų pavadinimas", + "value": "Reikšmė", + "ticks": "Padalos", + "arrow-and-scale-color": "Rodyklės ir skalės numatytoji spalva", + "scale-settings": "Skalės nustatymai", + "scale": "Skalė", + "scale-color": "Skalės spalvos", + "compass-appearance": "Kompaso išvaizda", + "label": "Etiketė", + "labels": "Etiketės", + "label-style": "Etiketės stilius", + "simple-gauge-type": "Tipas", + "gauge-bar-background": "Matuoklio juostos fonas", + "bar-color": "Juostos spalva", + "min-and-max-value": "Minimalios ir maksimalios reikšmės", + "min-and-max-label": "Minimalios ir maksimalios etiketės", + "font": "Šriftas", + "tick-width-and-color": "Padalų plotis ir spalva", + "min-max-validation-text": "Maksimali reikšmė turi būti didesnė nei minimali" + }, + "gpio": { + "pin": "Kaištis", + "label": "Etiketė", + "row": "Eilutė", + "column": "Stulpelis", + "color": "Spalva", + "panel-settings": "Skydelio nustatymai", + "background-color": "Fono spalva", + "gpio-switches": "GPIO jungikliai", + "no-gpio-switches": "GPIO jungikliai nesukonfigūruoti", + "add-gpio-switch": "Pridėti GPIO jungiklį", + "gpio-status-request": "GPIO būsenos užklausa", + "method-name": "Metodo pavadinimas", + "method-body": "Metodo turinys", + "gpio-status-change-request": "GPIO būsenos keitimo užklausa", + "parse-gpio-status-function": "GPIO būsenos analizės funkcija", + "gpio-leds": "GPIO šviesos diodai", + "no-gpio-leds": "GPIO šviesos diodai nesukonfigūruoti", + "add-gpio-led": "Pridėti GPIO šviesos diodą" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "Atributo parametras negali būti naudojamas šiame valdiklyje", + "blocked-location": "Geolokacija yra užblokuota jūsų naršyklėje", + "claim-device": "Priskirti įrenginį", + "claim-failed": "Nepavyko priskirti įrenginio!", + "claim-not-found": "Įrenginys nerastas!", + "claim-successful": "Įrenginys sėkmingai priskirtas!", + "date": "Data", + "device-name": "Įrenginio pavadinimas", + "device-name-required": "Įrenginio pavadinimas yra privalomas", + "discard-changes": "Atmesti pakeitimus", + "entity-attribute-required": "Objekto atributas yra privalomas", + "entity-coordinate-required": "Reikalingi abu laukai – platuma ir ilguma", + "entity-timeseries-required": "Objekto laiko seka yra privaloma", + "get-location": "Gauti dabartinę vietą", + "invalid-date": "Neteisinga data", + "latitude": "Platuma", + "longitude": "Ilguma", + "min-value-error": "Minimali reikšmė yra {{value}}", + "max-value-error": "Maksimali reikšmė yra {{value}}", + "not-allowed-entity": "Pasirinktas objektas negali turėti bendrų atributų", + "no-attribute-selected": "Nepasirinktas joks atributas", + "no-datakey-selected": "Nepasirinktas joks duomenų raktas", + "no-coordinate-specified": "Nenurodytas duomenų raktas platumai/ilgumai", + "no-entity-selected": "Nepasirinktas joks objektas", + "no-image": "Nėra paveikslėlio", + "no-support-geolocation": "Jūsų naršyklė nepalaiko geolokacijos", + "no-support-web-camera": "Jūsų naršyklė nepalaiko kamerų", + "enable-https-use-widget": "Įjunkite HTTPS, kad galėtumėte naudoti šį valdiklį", + "no-found-your-camera": "Kamera nerasta", + "no-permission-camera": "Naudotojas atmetė prieigą / svetainei nesuteikta leidimo naudoti kamerą", + "no-timeseries-selected": "Nepasirinkta jokia laiko seka", + "secret-key": "Slaptasis raktas", + "secret-key-required": "Slaptasis raktas yra privalomas", + "switch-attribute-value": "Perjungti objekto atributo reikšmę", + "switch-camera": "Perjungti kamerą", + "switch-timeseries-value": "Perjungti objekto laiko sekos reikšmę", + "take-photo": "Padaryti nuotrauką", + "time": "Laikas", + "timeseries-not-allowed": "Laiko sekos parametras negali būti naudojamas šiame valdiklyje", + "update-failed": "Atnaujinimas nepavyko", + "update-successful": "Atnaujinimas sėkmingas", + "update-attribute": "Atnaujinti atributą", + "update-timeseries": "Atnaujinti laiko seką", + "value": "Reikšmė", + "general-settings": "Bendrieji nustatymai", + "widget-title": "Valdiklio pavadinimas", + "claim-button-label": "Priskyrimo mygtuko etiketė", + "show-secret-key-field": "Rodyti laukelį „Slaptasis raktas“", + "labels-settings": "Etikečių nustatymai", + "show-labels": "Rodyti etiketes", + "device-name-label": "Etiketė įrenginio pavadinimo laukui", + "secret-key-label": "Etiketė slaptojo rakto laukui", + "messages-settings": "Pranešimų nustatymai", + "claim-device-success-message": "Tekstas, rodomas sėkmingai priskyrus įrenginį", + "claim-device-not-found-message": "Tekstas, rodomas, kai įrenginys nerastas", + "claim-device-failed-message": "Tekstas, rodomas nepavykus priskirti įrenginio", + "claim-device-name-required-message": "Klaidos pranešimas „Įrenginio pavadinimas privalomas“", + "claim-device-secret-key-required-message": "Klaidos pranešimas „Slaptasis raktas privalomas“", + "show-label": "Rodyti etiketę", + "label": "Etiketė", + "required": "Privaloma", + "required-error-message": "Klaidos pranešimas „Privaloma“", + "show-result-message": "Rodyti rezultatų pranešimą", + "integer-field-settings": "Sveikojo skaičiaus lauko nustatymai", + "min-value": "Minimali reikšmė", + "max-value": "Maksimali reikšmė", + "double-field-settings": "Skaičiaus su kableliu lauko nustatymai", + "text-field-settings": "Teksto lauko nustatymai", + "min-length": "Minimalus ilgis", + "max-length": "Maksimalus ilgis", + "checkbox-settings": "Žymimojo langelio nustatymai", + "true-label": "Pažymėto langelio etiketė", + "false-label": "Nepažymėto langelio etiketė", + "image-input-settings": "Paveikslėlio įvesties nustatymai", + "display-preview": "Rodyti peržiūrą", + "display-clear-button": "Rodyti išvalymo mygtuką", + "display-apply-button": "Rodyti taikymo mygtuką", + "display-discard-button": "Rodyti atmetimo mygtuką", + "datetime-field-settings": "Datos/laiko lauko nustatymai", + "display-time-input": "Rodyti laiko įvedimo lauką", + "latitude-key-name": "Platumos rakto pavadinimas", + "longitude-key-name": "Ilgumos rakto pavadinimas", + "show-get-location-button": "Rodyti mygtuką „Gauti dabartinę vietą“", + "use-high-accuracy": "Naudoti didelį tikslumą", + "location-fields-settings": "Vietos laukų nustatymai", + "latitude-label": "Etiketė platumai", + "longitude-label": "Etiketė ilgumai", + "input-fields-alignment": "Įvesties laukų lygiavimas", + "input-fields-alignment-column": "Stulpeliu (numatytasis)", + "input-fields-alignment-row": "Eilute", + "layout": "Išdėstymas", + "row-gap": "Tarpas tarp eilučių (pikseliais)", + "column-gap": "Tarpas tarp stulpelių (pikseliais)", + "latitude-field-required": "Platumos laukas yra privalomas", + "longitude-field-required": "Ilgumos laukas yra privalomas", + "attribute-settings": "Atributų nustatymai", + "widget-mode": "Valdiklio režimas", + "widget-mode-update-attribute": "Atnaujinti atributą", + "widget-mode-update-timeseries": "Atnaujinti laiko seką", + "attribute-scope": "Atributo sritis", + "attribute-scope-server": "Serverio atributas", + "attribute-scope-shared": "Bendras atributas", + "value-required": "Reikšmė privaloma", + "image-settings": "Paveikslėlio nustatymai", + "image-format": "Paveikslėlio formatas", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "Paveikslėlio kokybė (nuostolinis glaudinimas, pvz., JPEG ar WEBP)", + "max-image-width": "Maksimalus paveikslėlio plotis", + "max-image-height": "Maksimalus paveikslėlio aukštis", + "action-buttons": "Veiksmo mygtukai", + "show-action-buttons": "Rodyti veiksmo mygtukus", + "update-all-values": "Atnaujinti visas reikšmes, o ne tik pakeistas", + "save-button-label": "Mygtuko „IŠSAUGOTI“ etiketė", + "reset-button-label": "Mygtuko „ATŠAUKTI“ etiketė", + "group-settings": "Grupės nustatymai", + "show-group-title": "Rodyti pavadinimą laukų grupei, susijusiai su skirtingais objektais", + "group-title": "Grupės pavadinimas", + "fields-alignment": "Laukų lygiavimas", + "fields-alignment-row": "Eilute (numatytasis)", + "fields-alignment-column": "Stulpeliu", + "fields-in-row": "Laukų skaičius eilutėje", + "option-value": "Reikšmė („null“ – tuščiam pasirinkimui)", + "option-label": "Etiketė", + "hide-input-field": "Slėpti įvesties lauką", + "datakey-type": "Duomenų rakto tipas", + "datakey-type-server": "Serverio atributas (numatytasis)", + "datakey-type-shared": "Bendras atributas", + "datakey-type-timeseries": "Laiko seka", + "datakey-value-type": "Duomenų rakto reikšmės tipas", + "datakey-value-type-string": "Tekstas", + "datakey-value-type-double": "Skaičius su kableliu", + "datakey-value-type-integer": "Sveikasis skaičius", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Loginis (žymimasis langelis)", + "datakey-value-type-boolean-switch": "Loginis (jungiklis)", + "datakey-value-type-date-time": "Data ir laikas", + "datakey-value-type-date": "Data", + "datakey-value-type-time": "Laikas", + "datakey-value-type-select": "Pasirinkimas", + "datakey-value-type-radio": "Pasirinkimo mygtukas", + "datakey-value-type-color": "Spalva", + "value-is-required": "Reikšmė yra privaloma", + "ability-to-edit-attribute": "Atributo redagavimo galimybė", + "ability-to-edit-attribute-editable": "Redaguojamas (numatytasis)", + "ability-to-edit-attribute-disabled": "Išjungtas", + "ability-to-edit-attribute-readonly": "Tik skaitymui", + "disable-on-datakey-name": "Išjungti, jei kito duomenų rakto reikšmė yra neteisinga (nurodykite rakto pavadinimą)", + "field-appearance": "Lauko išvaizda", + "appearance-fill": "Užpildytas", + "appearance-outline": "Kontūrinis", + "subscript-sizing": "Indekso dydis", + "subscript-sizing-fixed": "Fiksuotas", + "subscript-sizing-dynamic": "Dinaminis", + "slide-toggle-settings": "Perjungimo slankiklio nustatymai", + "slide-toggle-label-position": "Perjungimo etiketės padėtis", + "slide-toggle-label-position-after": "Po", + "slide-toggle-label-position-before": "Prieš", + "select-options": "Pasirinkimo parinktys", + "no-select-options": "Pasirinkimo parinktys nesukonfigūruotos", + "add-select-option": "Pridėti pasirinkimo parinktį", + "numeric-field-settings": "Skaitinio lauko nustatymai", + "step-interval": "Reikšmių žingsnio intervalas", + "error-messages": "Klaidų pranešimai", + "min-value-error-message": "Klaidos pranešimas „Minimali reikšmė“", + "max-value-error-message": "Klaidos pranešimas „Maksimali reikšmė“", + "invalid-date-error-message": "Klaidos pranešimas „Neteisinga data“", + "invalid-JSON-error-message": "Klaidos pranešimas „Neteisingas JSON formatas“", + "icon-settings": "Piktogramos nustatymai", + "dialog-editor-settings": "Dialogo redaktoriaus nustatymai", + "use-custom-icon": "Naudoti tinkintą piktogramą", + "input-cell-icon": "Piktograma prieš įvesties laukelį", + "value-conversion-settings": "Reikšmės konvertavimo nustatymai", + "get-value-settings": "Reikšmės gavimo nustatymai", + "use-get-value-function": "Naudoti getValue funkciją", + "get-value-function": "getValue funkcija", + "set-value-settings": "Reikšmės nustatymo nustatymai", + "use-set-value-function": "Naudoti setValue funkciją", + "set-value-function": "setValue funkcija", + "json-invalid": "JSON reikšmė turi neteisingą formatą", + "title": "Pavadinimas", + "cancel-button-label": "Mygtuko „Atšaukti“ etiketė", + "radio-button-settings": "Pasirinkimo mygtuko nustatymai", + "color": "Spalva", + "columns": "Stulpeliai", + "radio-options": "Pasirinkimo parinktys", + "no-radio-options": "Pasirinkimo parinktys nesukonfigūruotos", + "add-radio-option": "Pridėti pasirinkimo parinktį", + "radio-label-position": "Etiketės padėtis", + "radio-label-position-before": "Prieš", + "radio-label-position-after": "Po" + }, + "invalid-qr-code-text": "Neteisingas įvestas tekstas QR kodui. Įvestis turi būti teksto tipo", + "qr-code": { + "use-qr-code-text-function": "Naudoti QR kodo teksto funkciją", + "qr-code-text-pattern": "QR kodo teksto šablonas (pvz. '${entityName} | ${keyName} - papildomas tekstas.')", + "qr-code-text-pattern-hint": "QR kodo teksto šablonas naudoja pirmo rasto rakto reikšmę iš objektų, esančių objekto pseudonime.", + "qr-code-text-pattern-required": "QR kodo teksto šablonas yra privalomas.", + "qr-code-text-function": "QR kodo teksto funkcija" + }, + "label-widget": { + "label-pattern": "Šablonas", + "label-pattern-hint": "Patarimas: pvz. 'Tekstas ${keyName} vienetai.' arba ${#<rakto indeksas>} vienetai'", + "label-pattern-required": "Šablonas yra privalomas", + "label-position": "Padėtis (procentais fone)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Fono spalva", + "font-settings": "Šrifto nustatymai", + "background-image": "Fono paveikslėlis", + "labels": "Etiketės", + "no-labels": "Etiketės nesukonfigūruotos", + "add-label": "Pridėti etiketę" + }, + "navigation": { + "title": "Pavadinimas", + "navigation-path": "Naršymo kelias", + "filter-type": "Filtro tipas", + "filter-type-all": "Visi elementai", + "filter-type-include": "Įtraukti elementus", + "filter-type-exclude": "Išskirti elementus", + "items": "Elementai", + "enter-urls-to-filter": "Įveskite URL filtravimui..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Pranešimo tipas", + "method": "Metodas", + "params": "Parametrai", + "created-time": "Sukūrimo laikas", + "expiration-time": "Galiojimo pabaigos laikas", + "retries": "Bandymų skaičius", + "status": "Būsena", + "filter": "Filtras", + "refresh": "Atnaujinti", + "add": "Pridėti RPC užklausą", + "details": "Išsamiau", + "delete": "Ištrinti", + "delete-request-title": "Ištrinti nuolatinę RPC užklausą", + "delete-request-text": "Ar tikrai norite ištrinti šią užklausą?", + "details-title": "Išsami informacija RPC ID: ", + "additional-info": "Papildoma informacija", + "response": "Atsakas", + "any-status": "Bet kuri būsena", + "rpc-status-list": "RPC būsenų sąrašas", + "no-request-prompt": "Nėra rodomų užklausų", + "send-request": "Siųsti užklausą", + "add-title": "Sukurti nuolatinę RPC užklausą", + "method-error": "Metodas yra privalomas.", + "timeout-error": "Minimalus laukimo laikas yra 5000 (5 sekundės).", + "white-space-error": "Tarpai neleidžiami.", + "rpc-status": { + "QUEUED": "EILĖJE", + "SENT": "IŠSIŲSTA", + "DELIVERED": "PRISTATYTA", + "SUCCESSFUL": "SĖKMINGA", + "TIMEOUT": "PASIBAIGĖ LAIKAS", + "EXPIRED": "NEBEGALIOJA", + "FAILED": "NEPAVYKO" + }, + "rpc-search-status-all": "VISOS", + "message-types": { + "false": "Dvipusis", + "true": "Vienpusis" + }, + "general-settings": "Bendrieji nustatymai", + "enable-filter": "Įjungti filtrą", + "enable-sticky-header": "Rodyti antraštę slenkant", + "enable-sticky-action": "Rodyti veiksmų stulpelį slenkant", + "display-request-details": "Rodyti užklausos detales", + "allow-send-request": "Leisti siųsti RPC užklausą", + "allow-delete-request": "Leisti ištrinti RPC užklausą", + "columns-settings": "Stulpelių nustatymai", + "display-columns": "Rodytini stulpeliai", + "column": "Stulpelis", + "no-columns-found": "Nerasta jokių stulpelių", + "no-columns-matching": "„{{column}}“ nerasta." + }, + "range-chart": { + "chart": "Diagrama", + "data-zoom": "Duomenų mastelio keitimas", + "range-chart-appearance": "Intervalinės diagramos išvaizda", + "range-colors": "Intervalo spalvos", + "out-of-range-color": "Už intervalo ribų spalva", + "show-range-thresholds": "Rodyti intervalo ribas", + "range-thresholds-settings": "Intervalo ribų nustatymai", + "fill-area": "Užpildymo sritis", + "fill-area-opacity": "Užpildymo srities nepermatomumas", + "range-chart-style": "Intervalinės diagramos stilius" + }, + "knob": { + "behavior": "Elgsena", + "initial-value": "Pradinė reikšmė", + "initial-value-hint": "Veiksmas, skirtas gauti pradinę rankenėlės reikšmę.", + "on-value-change": "Pasikeitus reikšmei", + "on-value-change-hint": "Veiksmas, vykdomas, kai rankenėlės reikšmė pasikeičia.", + "range": "Intervalas", + "min": "min", + "max": "maks", + "value": "Reikšmė", + "fallback-initial-value": "Atsarginė pradinė reikšmė" + }, + "rpc": { + "value-settings": "Reikšmės nustatymai", + "initial-value": "Pradinė reikšmė", + "retrieve-value-settings": "Įjungimo/išjungimo reikšmės gavimo nustatymai", + "retrieve-value-method": "Reikšmės gavimo metodas", + "retrieve-value-method-none": "Negauti reikšmės", + "retrieve-value-method-rpc": "Gauti reikšmę naudojant RPC metodą", + "retrieve-value-method-attribute": "Prenumeruoti atributą", + "retrieve-value-method-timeseries": "Prenumeruoti laiko seką", + "attribute-value-key": "Atributo raktas", + "timeseries-value-key": "Laiko sekos raktas", + "get-value-method": "RPC reikšmės gavimo metodas", + "parse-value-function": "Reikšmės analizės funkcija", + "update-value-settings": "Reikšmės atnaujinimo nustatymai", + "set-value-method": "RPC reikšmės nustatymo metodas", + "convert-value-function": "Reikšmės konvertavimo funkcija", + "rpc-settings": "RPC nustatymai", + "request-timeout": "RPC užklausos laukimo laikas (ms)", + "persistent-rpc-settings": "Nuolatinės RPC nustatymai", + "request-persistent": "Nuolatinė RPC užklausa", + "persistent-polling-interval": "Apklausos intervalas (ms) nuolatinei RPC komandai gauti", + "common-settings": "Bendrieji nustatymai", + "switch-title": "Jungiklio pavadinimas", + "show-on-off-labels": "Rodyti įjungta/išjungta etiketes", + "slide-toggle-label": "Perjungimo etiketė", + "label-position": "Etiketės padėtis", + "label-position-before": "Prieš", + "label-position-after": "Po", + "slider-color": "Slankiklio spalva", + "slider-color-primary": "Pagrindinė", + "slider-color-accent": "Akcentinė", + "slider-color-warn": "Įspėjamoji", + "button-style": "Mygtuko stilius", + "button-raised": "Iškilęs mygtukas", + "button-primary": "Pagrindinė spalva", + "button-background-color": "Mygtuko fono spalva", + "button-text-color": "Mygtuko teksto spalva", + "widget-title": "Valdiklio pavadinimas", + "button-label": "Mygtuko etiketė", + "device-attribute-scope": "Įrenginio atributo sritis", + "server-attribute": "Serverio atributas", + "shared-attribute": "Bendras atributas", + "device-attribute-parameters": "Įrenginio atributo parametrai", + "is-one-way-command": "Vienpusė komanda", + "rpc-method": "RPC metodas", + "rpc-method-params": "RPC metodo parametrai", + "show-rpc-error": "Rodyti RPC komandos vykdymo klaidą", + "led-title": "LED pavadinimas", + "led-color": "LED spalva", + "check-status-settings": "Būsenos tikrinimo nustatymai", + "perform-rpc-status-check": "Vykdyti RPC įrenginio būsenos patikrinimą", + "retrieve-led-status-value-method": "LED būsenos gavimo metodas", + "led-status-value-attribute": "Įrenginio atributas, kuriame saugoma LED būsena", + "led-status-value-timeseries": "Įrenginio laiko seka, kurioje saugoma LED būsena", + "check-status-method": "RPC įrenginio būsenos tikrinimo metodas", + "parse-led-status-value-function": "LED būsenos reikšmės analizės funkcija", + "knob-title": "Rankenėlės pavadinimas" + }, + "maps": { + "map-type": { + "type": "Žemėlapio tipas", + "map": "Žemėlapis", + "image": "Paveikslėlis" + }, + "image": { + "image-source": "Paveikslėlio šaltinis", + "image-source-image": "Paveikslėlis", + "image-source-entity-key": "Objekto raktas", + "source-entity-alias": "Šaltinio objekto pseudonimas", + "image-url-key": "Paveikslėlio URL raktas", + "image-url-key-required": "Paveikslėlio URL raktas yra privalomas" + }, + "control": { + "map-controls": "Žemėlapio valdikliai", + "position": "Padėtis", + "position-topleft": "Viršuje kairėje", + "position-topright": "Viršuje dešinėje", + "position-bottomleft": "Apačioje kairėje", + "position-bottomright": "Apačioje dešinėje", + "zoom-actions": "Mastelio keitimo veiksmai", + "zoom-scroll": "Slinkimas", + "zoom-double-click": "Dvigubas paspaudimas", + "zoom-control-buttons": "Valdymo mygtukai", + "scale": "Mastelis", + "scale-metric": "Metrinis", + "scale-imperial": "Imperinis", + "switch-to-drag-mode-using-button": "Perjungti į tempimo režimą naudojant mygtuką" + }, + "timeline": { + "control-panel": "Laiko juostos valdymo skydelis", + "time-step": "Laiko žingsnis", + "speed-options": "Greičio parinktys", + "timestamp": "Laiko žyma", + "snap-to-real-location": "Pririšti prie tikros vietos", + "location-snap-filter-function": "Vietos pririšimo filtravimo funkcija", + "no-trips-data-available": "Kelionių duomenų nėra" + }, + "map-action": { + "map-action-buttons": "Žemėlapio veiksmų mygtukai", + "label": "Etiketė", + "icon": "Piktograma", + "color": "Spalva", + "action": "Veiksmas", + "add-button": "Pridėti mygtuką", + "no-action-buttons-configured": "Veiksmų mygtukai nesukonfigūruoti", + "remove-action-button": "Pašalinti veiksmų mygtuką", + "map-action-button": "Žemėlapio veiksmų mygtukas", + "button-requires": "Mygtukui reikalinga etiketė arba piktograma" + }, + "common": { + "common-map-settings": "Bendrieji žemėlapio nustatymai", + "fit-map-bounds": "Pritaikyti žemėlapio ribas, kad apimtų visus žymeklius", + "default-map-center-position": "Numatytoji žemėlapio centro padėtis", + "default-map-zoom-level": "Numatytasis žemėlapio mastelio lygis", + "entities-limit": "Įkeltinų objektų riba" + }, + "layer": { + "label": "Etiketė", + "layer": "Sluoksnis", + "layers": "Sluoksniai", + "map-layers": "Žemėlapio sluoksniai", + "add-layer": "Pridėti sluoksnį", + "layer-settings": "Sluoksnio nustatymai", + "remove-layer": "Pašalinti sluoksnį", + "no-layers": "Sluoksniai nesukonfigūruoti", + "roadmap": "Kelio žemėlapis", + "satellite": "Palydovinis", + "hybrid": "Hibridinis", + "reference": { + "reference-layer": "Etaloninis sluoksnis", + "no-layer": "Nėra sluoksnio", + "openstreetmap-hybrid": "OpenStreetMap hibridinis", + "world-edition-hybrid": "World Edition hibridinis", + "enhanced-contrast-hybrid": "Padidinto kontrasto hibridinis" }, - "column-type-required": "Stulpelio tipas būtinas.", - "entity-field": { - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "type": "Tipas", - "device_profile": "Device profile", - "asset_profile": "Asset profile", - "assigned_customer": "Priskirtas klientas", - "authority": "Authority", - "first_name": "Vardas", - "last_name": "Pavardė", - "email": "El. paštas", - "title": "Pavadinimas", - "country": "Šalis", - "state": "Apskritis/rajonas", - "city": "Miestas", - "address": "Adresas", - "address2": "Adresas 2", - "zip": "Pašto kodas", - "phone": "Tel, numeris", - "label": "Etiketė" + "provider": { + "provider": "Tiekėjas", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "WorldStreetMap", + "esri-topo": "WorldTopoMap", + "esri-imagery": "WorldImagery", + "cartodb-positron": "Positron", + "cartodb-dark-matter": "DarkMatter" + }, + "google": { + "title": "Google", + "roadmap": "Kelio žemėlapis", + "satellite": "Palydovinis", + "hybrid": "Hibridinis", + "terrain": "Reljefas" + }, + "here": { + "title": "HERE", + "normal-day": "Įprasta diena", + "normal-night": "Įprasta naktis", + "hybrid-day": "Hibridinė diena", + "terrain-day": "Reljefinė diena" + }, + "tencent": { + "title": "Tencent", + "normal": "Įprastas", + "satellite": "Palydovinis", + "terrain": "Reljefinis" + }, + "custom": { + "title": "Tinkintas", + "tile-url": "Plytelės URL" + } }, - "sort-order": { - "asc": "Didėjanti", - "desc": "Mažėjanti", - "none": "Nėra" + "credentials": { + "credentials": "Prisijungimo duomenys", + "api-key": "API raktas" + } + }, + "overlays": { + "overlays": "Perdangos", + "overlays-hint": "Sukonfigūruokite duomenų šaltinius, išvaizdą, elgseną, redagavimo parinktis ir objektų grupavimą žemėlapyje", + "trips": "Kelionės", + "markers": "Žymekliai", + "polygons": "Daugiakampiai", + "circles": "Apskritimai" + }, + "data-layer": { + "source": "Šaltinis", + "filter": "Filtras", + "additional-data-keys": "Papildomi duomenų raktai", + "additional-datasources": "Papildomi duomenų šaltiniai", + "additional-datasources-hint": "Duomenų šaltinis, skirtas pasiekti atributus ar telemetriją iš objektų, nerodomų žemėlapyje; gali būti naudojamas žemėlapio perdangų funkcijose.", + "more-datasources": "Daugiau duomenų šaltinių", + "data-keys": "Duomenų raktai", + "add-datasource": "Pridėti duomenų šaltinį", + "no-datasources": "Duomenų šaltiniai nesukonfigūruoti", + "remove-datasource": "Pašalinti duomenų šaltinį", + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, vykdomas, kai naudotojas paspaudžia ant žemėlapio elemento.", + "groups": "Grupės", + "groups-hint": "Grupių pavadinimų sąrašas, priskirtas perdangai, naudojamas jos matomumui žemėlapyje keisti.", + "color": "Spalva", + "color-settings": "Spalvos nustatymai", + "color-type-constant": "Pastovi", + "color-type-range": "Intervalas", + "color-type-function": "Funkcija", + "color-range-source-key": "Spalvų intervalo šaltinio raktas", + "color-range-source-key-required": "Spalvų intervalo šaltinio raktas yra privalomas", + "color-range": "Spalvų intervalas", + "color-function": "Spalvos funkcija", + "label": "Etiketė", + "tooltip": "Patarimas", + "pattern-type-pattern": "Šablonas", + "pattern-type-function": "Funkcija", + "label-pattern": "Etiketė (šablonų pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "label-function": "Etiketės funkcija", + "tooltip-pattern": "Patarimas (pvz. 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "tooltip-function": "Patarimo funkcija", + "tooltip-trigger": "Patarimo aktyvatorius", + "tooltip-trigger-click": "Rodyti patarimą paspaudus", + "tooltip-trigger-hover": "Rodyti patarimą užvedus žymeklį", + "auto-close-tooltips": "Automatiškai uždaryti patarimus", + "tooltip-offset": "Patarimo poslinkis", + "tooltip-offset-horizontal": "Horizontalus", + "tooltip-offset-vertical": "Vertikalus", + "tooltip-tag-actions": "Žymų veiksmai", + "add-tooltip-tag-action": "Pridėti žymos veiksmą", + "edit-tooltip-tag-action": "Redaguoti žymos veiksmą", + "remove-tooltip-tag-action": "Pašalinti žymos veiksmą", + "action-add": "Pridėti", + "action-edit": "Redaguoti", + "action-move": "Perkelti", + "action-remove": "Pašalinti", + "edit-instruments": "Įrankiai", + "persist-location-attribute-scope": "Atributo sritis vietos išsaugojimui", + "enable-snapping": "Įjungti pritraukimą prie kitų viršūnių tiksliam braižymui", + "enable-snapping-hint": "Automatiškai sulygiuoja naujus taškus su esamomis formomis, kad braižymas būtų tikslesnis.", + "drag-drop-mode": "Vilkimo režimas", + "trip": { + "no-trips": "Kelionės nesukonfigūruotos", + "add-trip": "Pridėti kelionę", + "trip-configuration": "Kelionės konfigūracija", + "remove-trip": "Pašalinti kelionę" }, - "details-mode": { - "on-row-click": "Spustelėjus eilutėje", - "on-action-button-click": "Paspaudus informacijos mygtuką", - "disabled": "Išjungta" + "marker": { + "marker": "Žymeklis", + "latitude-key": "Platumos raktas", + "longitude-key": "Ilgumos raktas", + "x-pos-key": "X padėties raktas", + "y-pos-key": "Y padėties raktas", + "latitude-key-required": "Platumos raktas yra privalomas", + "longitude-key-required": "Ilgumos raktas yra privalomas", + "x-pos-key-required": "X padėties raktas yra privalomas", + "y-pos-key-required": "Y padėties raktas yra privalomas", + "no-markers": "Žymekliai nesukonfigūruoti", + "add-marker": "Pridėti žymeklį", + "marker-configuration": "Žymeklio konfigūracija", + "remove-marker": "Pašalinti žymeklį", + "marker-type": "Žymeklio tipas", + "marker-type-shape": "Forma", + "marker-type-icon": "Piktograma", + "marker-type-image": "Paveikslėlis", + "shape": "Forma", + "icon": "Piktograma", + "image": "Paveikslėlis", + "marker-shapes": "Žymeklio formos", + "marker-icon": "Žymeklio piktograma", + "marker-appearance": "Žymeklio išvaizda", + "marker-image": "Žymeklio paveikslėlis", + "marker-image-type-image": "Paveikslėlis", + "marker-image-type-function": "Funkcija", + "custom-marker-image-size": "Tinkintas žymeklio paveikslėlio dydis", + "marker-image-function": "Žymeklio paveikslėlio funkcija", + "marker-images": "Žymeklio paveikslėliai", + "marker-offset": "Žymeklio poslinkis", + "offset-horizontal": "Horizontalus", + "offset-vertical": "Vertikalus", + "rotate-marker": "Pasukti žymeklį", + "offset-angle": "Poslinkio kampas", + "position-conversion": "Padėties konversija", + "position-conversion-function": "Padėties konversijos funkcija, turi grąžinti x,y koordinates (dvigubo tipo) nuo 0 iki 1", + "clustering": { + "use-map-markers-clustering": "Naudoti žemėlapio žymeklių grupavimą", + "zoom-on-cluster-click": "Priartinti paspaudus ant grupės", + "max-zoom": "Didžiausias mastelio lygis, kai žymeklis gali priklausyti grupei (0–18)", + "max-radius": "Didžiausias spindulys, kurį apima grupė", + "zoom-animation": "Žymeklių animacija keičiat mastelį", + "bounds-on-cluster-mouse-over": "Rodyti žymeklių ribas užvedus pelę ant grupės", + "spiderfy-max-zoom-level": "Išskleisti grupės žymeklius esant didžiausiam masteliui (kad matytųsi visi)", + "load-optimization": "Įkėlimo optimizavimas", + "chunked-load": "Naudoti dalinį įkėlimą, kad puslapis neužstrigtų", + "lazy-load": "Naudoti uždelstą įkėlimą žymekliams pridėti", + "use-cluster-marker-color-function": "Naudoti grupės žymeklių spalvos funkciją", + "marker-color-function": "Žymeklio spalvos funkcija" + }, + "edit": "Redaguoti žymeklį", + "remove-marker-for": "Pašalinti žymeklį objektui '{{entityName}}'", + "place-marker": "Padėti žymeklį", + "place-marker-hint": "Spustelėkite, kad padėtumėte žymeklį", + "place-marker-hint-with-entity": "Spustelėkite, kad padėtumėte objektą '{{entityName}}'" }, - "change-owner": "Pakeisti savininką", - "select-target-owner": "Pasirinkite savininką", - "no-owners-matching": "Savinikų atitinkančių '{{owner}}' nėra.", - "target-owner-required": "Savininkas būtinas.", - "confirm-change-owner-title": "Ar tikrai norite pakeisti savininką { count, plural, =1 {1 pasirinktam subjektui} other {# pasirinktiems subjektams} }?", - "confirm-change-owner-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektai iš esamo savininko bus perkelti į naujo savininko grupę 'Visi'.", - "add-to-group": "Pridėti į grupę", - "move-to-group": "Perkelti į grupę", - "select-entity-group": "Pasirinkite subjektų grupę", - "no-entity-groups-matching": "Subjektų grupių, atitinkančių '{{entityGroup}}' nėra.", - "create-new-entity-group": "Sukurti naują!", - "target-entity-group-required": "Subjektų grupė būtina.", - "select-user-group": "Pasirinkite vartotojų grupę", - "no-user-groups-matching": "Vartotojų grupių, atitinkančių '{{entityGroup}}' nėra.", - "target-user-group-required": "Vartotojų grupė būtina.", - "remove-from-group": "Pašalinti iš grupės", - "group-table-title": "Grupių lentelės pavadinimas", - "enable-search": "Leisti subjektų paiešką", - "enable-add": "Leisti pridėti subjektus", - "enable-delete": "Leisti panaikinti subjektus", - "enable-selection": "Leisti subjektų pasirinkimą", - "enable-group-transfer": "Leisti perkėlimus tarp grupių", - "display-pagination": "Rodyti dalinimą puslapiais", - "default-page-size": "Numatytasis puslapio dydis", - "enable-assignment-actions": "Leisti priskirti", - "enable-credentials-management": "Leisti valdyti įgaliojimus", - "enable-login-as-user": "Leisti prisijungti kaip vartotojui", - "enable-users-management": "Leisti valdyti vartotojus", - "enable-customers-management": "Leisti valdyti klientus", - "enable-assets-management": "Leisti valdyti turtą", - "enable-devices-management": "Leisti valdyti įrenginius", - "enable-entity-views-management": "Leisti subjektų rodinių valdymą", - "enable-edges-management": "Enable edges management", - "enable-dashboards-management": "Leisti skydelių valdymą", - "enable-scheduler-events-management": "Leisti tvarkaraščio įvykių valdymą", - "open-details-on": "Atverti subjekto informaciją", - "select-existing": "Pasirinkti egzistuojančią subjektų grupę", - "create-new": "Sukurti naują subjektų grupę", - "new-entity-group-name": "Naujos subjektų grupės pavadinimas", - "entity-group-list": "Subjektų grupių sąrašas", - "entity-group-list-empty": "Nepasirinktos subjektų grupės.", - "name-starts-with": "Subjektų grupės pavadinimas prasideda", - "entity-group-name-filter-required": "Subjektų grupės filtras būtinas.", - "roles": "Rolės", - "permissions": "Leidimai", - "public": "Vieša", - "entity-group-public": "Subjektų grupė yra vieša", - "make-public": "Subjektų grupę padaryti vieša", - "make-private": "Subjektų grupę padaryti privačia", - "make-public-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti vieša?", - "make-public-entity-group-text": "Po patvirtinimo subjektų grupė ir visi jai priklausantys subjektai taps vieši ir matomi kitiems vartotojams.", - "make-private-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti privačia?", - "make-private-entity-group-text": "Po patvirtinimo, subjektų grupė ir visi jai priklausantys subjektai taps privatūs ir nematomi kitiems vartotojams.", - "share": "Pasdalinti grupe", - "copyId": "Kopijuoti subjektų grupės Id", - "idCopiedMessage": "Subjektų grupės Id nukopijuotas į škarpinę", - "entity-group-name": "Subjektų grupės pavadinimas", - "all-users": "Visi vartotojai", - "owner": "Savininkas", - "owner-required": "Savininkas būtinas.", - "manage-owner-and-groups": "Savininkas ir grupės", - "owner-and-groups": "Savininkas ir grupės" - }, - "entity-field": { - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "type": "Tipas", - "first-name": "Vardas", - "last-name": "Pavardė", - "email": "El. paštas", - "title": "Pavadinimas", - "country": "Šalis", - "state": "Rajonas", - "city": "Miestas", - "address": "Addresas", - "address2": "Addresas 2", - "zip": "Pašto kodas", - "phone": "Telefonas", - "label": "Etikelė", - "configuration": "Konfigūracija", - "schedule": "Grafikas", - "originatorId": "Iniciatoriaus Id", - "originatorType": "Iniciatoriaus tipas" - }, - "entity-view": { - "all": "Visi", - "all-entity-views": "Visi subjektų rodiniai", - "groups": "Grupės", - "shared": "bendrinami", - "entity-view": "Subjektų rodiniai", - "entity-view-required": "Subjeto rodinys būtinas.", - "entity-views": "Subjektų rodiniai", - "management": "Subjektų rodinių valdymas", - "view-entity-views": "Peržiūrėti subjektų rodinius", - "entity-view-alias": "Subjektų rodinio pseudonimas", - "aliases": "Subjektų rodinio pseudonimai", - "no-alias-matching": "'{{alias}}' nerastas.", - "no-aliases-found": "Pseudonimų nėra.", - "no-key-matching": "'{{key}}' nerastas.", - "no-keys-found": "Raktų nėra.", - "create-new-alias": "Sukurti naują!", - "create-new-key": "Sukurti naują!", - "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", - "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", - "no-entity-views-matching": "Subjektų rodinių, atitinkančių '{{entity}}' nėra.", - "public": "Viešas", - "alias": "Pseudonimas", - "alias-required": "Subjekto rodinio pseudonimas būtinas.", - "remove-alias": "Panaikinti subjekto rodinio pseudonimą", - "add-alias": "Pridėti subjekto rodinio pseudonimą", - "name-starts-with": "Subjekto rodinio pavadinimas prasideda", - "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%subjekto_rodinio_pavadinimo_fragmentas%', '%subjekto_rodinio_pavadinimo_pabaiga', 'subjekto_rodinio_pavadinimo_pradžia%'.", - "entity-view-list": "Subjektų rodinių sąrašas", - "use-entity-view-name-filter": "Naudoti filtrą", - "entity-view-list-empty": "Subjektai nepasirinkti.", - "entity-view-name-filter-required": "Subjekto rodinio pavadinimo filtras būtinas.", - "entity-view-name-filter-no-entity-view-matched": "Subjektų rodinių, kurių pavadinimai prasideda '{{entityView}}' nėra.", - "add": "Pridėti subjektų rodinį", - "entity-view-public": "Subjektų rodnys yra viešas", - "assign-to-customer": "Priskirti klientui", - "assign-entity-view-to-customer": "Subjektų rodinį (-ius) priskirti klientui", - "assign-entity-view-to-customer-text": "Pasirinkite subjektų rodinius, kuriuos norite priskirti klientui", - "no-entity-views-text": "Subjektų rodinių nėra", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti subjektų rodinį (-ius)", - "entity-view-details": "Informacija apie subjektų rodinį", - "add-entity-view-text": "Pridėti naują subjektų rodinį", - "delete": "Panikinti subjektų rodinį", - "assign-entity-views": "Priskirti subjektų rodinius", - "assign-entity-views-text": "Priskirti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } klientui", - "delete-entity-views": "Panaikinti subjektų rodinius", - "make-public": "Subjektų rodinį padaryti viešu", - "make-private": "Subjektų rodinį padaryti privačiu", - "unassign-from-customer": "Atsieti nuo kliento", - "unassign-entity-views": "Atsieti subjektų rodinius", - "unassign-entity-views-action-title": "Atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } nuo kliento", - "assign-new-entity-view": "Priskirti naują subjektų rodinį", - "delete-entity-view-title": "Ar tikrai norite panaikinti subjektų rodinį '{{entityViewName}}'?", - "delete-entity-view-text": "Būkite dėmesingi, po patvirtinimo, subjektų rodinys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "delete-entity-views-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subketų rodinį} other {# subjektų rodinius} }?", - "delete-entity-views-action-title": "Panaikinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }", - "delete-entity-views-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektų rodiniai ir visa su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "make-public-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti viešu?", - "make-public-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps vieša ir matoma kitiems vartotojams.", - "make-private-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti privačiu?", - "make-private-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps privati ir kiti vartotojai jų nebematys.", - "unassign-entity-view-title": "Ar tikrai norite atsieti subjektų rodinį '{{entityViewName}}'?", - "unassign-entity-view-text": "Po patvirtinimo subjektų rodinys bus atsietas ir klientas jo nebematys.", - "unassign-entity-view": "Atsieti subjektų rodinį", - "unassign-entity-views-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", - "unassign-entity-views-text": "Po patvirtinimo visi pasirinkti subjektų rodiniai bus atsieti ir klientas jų nebematys.", - "entity-view-type": "Subjektų rodinio tipas", - "entity-view-type-required": "Subjektų rodinio tipas būtinas.", - "select-entity-view-type": "Pasirinkite subjektų rodinio tipą", - "enter-entity-view-type": "Įveskite subjektų rodinio tipą", - "any-entity-view": "Bet kuris subjektų tipas", - "no-entity-view-types-matching": "Subjektų rodinių, atitinkančių '{{entitySubtype}}' nėra.", - "entity-view-type-list-empty": "Subjektų tipai nepasirinkti.", - "entity-view-types": "Subjektų rodinio tipai", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "type-max-length": "Rodinio tipas negali viršyti 256 simbolių", - "description": "Aprašymas", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti subjekto rodinio Id", - "idCopiedMessage": "Subjektų rodinio Id nukopjuotas į iškarpinę", - "assignedToCustomer": "Priskirtas klientui", - "unable-entity-view-device-alias-title": "Subjektų rodinio pseudonimo ištrinti nepavyko", - "unable-entity-view-device-alias-text": "Įrenginio pseudonimas '{{entityViewAlias}}' Negali būti ištrintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", - "select-entity-view": "Pasirinkti subjektų rodinį", - "start-date": "Pradžios data", - "start-ts": "Pradžios laikas", - "end-date": "Pabaigos data", - "end-ts": "Pabaigos laikas", - "date-limits": "Datos limitas", - "client-attributes": "Kliento atributai", - "shared-attributes": "Bendrinami atributai", - "server-attributes": "Serverio atributai", - "timeseries": "Telemetrija", - "client-attributes-placeholder": "Kliento atributai", - "shared-attributes-placeholder": "Bendrinami atributai", - "server-attributes-placeholder": "Serverio atributai", - "timeseries-placeholder": "Telemetrija", - "target-entity": "Tikslinis subjektas", - "attributes-propagation": "Atributų paplitimas", - "attributes-propagation-hint": "Subjektų rodinys automatikškai kopijuos nurodytus atributus iš tikslinio subjekto kiekvieną kartą, kai išsaugosite ar atnaujinsite šį rodinį. Norint išsaugoti sistemos spartą, tikslinio subjekto atributai neperkeliami į subjektų rodinį kiekvieną kartą kai jie pasikeičia. Tačiau jūs galite įjungti automatinį atributų reikšmių perdavimą, taisyklių grandinėje sukonfigūravę \"copy to view\" taisyklę ir \"Post attributes\" bei \"Attributes Updated\" žinutes nukreipę į naują taisyklę.", - "timeseries-data": "Temetrijos duomenys", - "timeseries-data-hint": "Sukonfigūruokite tikslinio objekto telemetrijos duomenų raktus, kurie bus pasiekiami objekto rodinyje. Šios telemetrijos duomenys yra tik skaitomi.", - "select-group-to-add": "Pasirinkite grupę į kurią pridėti pažymėtus subjektų rodinius", - "select-group-to-move": "Pasirinkite grupę į kurią perkelti pažymėtus subjektų rodinius", - "remove-entity-views-from-group": "Ar tikrai norite iš '{{entityGroup}}' grupės pašalinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", - "group": "Subjektų rodinių grupė", - "list-of-groups": "{ count, plural, =1 {Viena subjektų rodinių grupė} other {# subjektų rodinių grupių sąrašas} }", - "group-name-starts-with": "Subjektų rodinių grupės pavadinimas prasideda '{{prefix}}'", - "search": "Subjektų rodinių paieška", - "selected-entity-views": "Pasirinkta { count, plural, =1 {1 subjektų rodinys} other {# subjektų rodiniai} }", - "assign-entity-view-to-edge": "Assign Entity View(s) To Edge", - "assign-entity-view-to-edge-text": "Please select the entity views to assign to the edge", - "unassign-entity-view-from-edge-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", - "unassign-entity-view-from-edge-text": "After the confirmation the entity view will be unassigned and won't be accessible by the edge.", - "unassign-entity-views-from-edge-action-title": "Unassign { count, plural, =1 {1 entity view} other {# entity views} } from edge", - "unassign-entity-view-from-edge": "Unassign entity view", - "unassign-entity-views-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 entity view} other {# entity views} }?", - "unassign-entity-views-from-edge-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the edge." - }, - "event": { - "events": "Įvykiai", - "event-type": "Įvykio tipas", - "events-filter": "Įvykių filtras", - "clean-events": "Įšvalyti įvykius", - "type-error": "Klaida", - "type-lc-event": "Gyvavimo ciklo įvykis", - "type-stats": "Statistika", - "type-debug-converter": "Debug", - "type-debug-integration": "Debug", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", - "no-events-prompt": "Įvykių nėra", - "error": "Klaida", - "alarm": "Įspėjimas", - "event-time": "Įvykio laikas", - "server": "Serveris", - "body": "Body", - "method": "Metodas", - "type": "Tipas", - "in": "Į", - "out": "Iš", - "metadata": "Metadata", - "message": "Message", - "entity": "Entity", - "message-id": "Message Id", - "copy-message-id": "Copy message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "data": "Data", - "event": "Event", - "status": "Status", - "success": "Success", - "failed": "Failed", - "messages-processed": "Messages processed", - "max-messages-processed": "Maximum messages processed", - "min-messages-processed": "Minimum messages processed", - "errors-occurred": "Errors occurred", - "max-errors-occurred": "Maximum errors occurred", - "min-errors-occurred": "Minimum errors occurred", - "min-value": "Minimum value is 0.", - "all-events": "All", - "has-error": "Has error", - "entity-id": "Entity Id", - "copy-entity-id": "Copy entity Id", - "entity-type": "Entity type", - "clear-filter": "Clear Filter", - "clear-request-title": "Clear all events", - "clear-request-text": "Are you sure you want to clear all events?", - "started": "Started", - "updated": "Updated", - "stopped": "Stopped" - }, - "extension": { - "extensions": "Extensions", - "selected-extensions": "{ count, plural, =1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", - "id": "Id", - "extension-id": "Extension id", - "extension-type": "Extension type", - "transformer-json": "JSON *", - "unique-id-required": "Current extension id already exists.", - "delete": "Delete extension", - "add": "Add extension", - "edit": "Edit extension", - "view": "View extension", - "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", - "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", - "delete-extensions-title": "Are you sure you want to delete { count, plural, =1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", - "converters": "Converters", - "converter-id": "Converter id", - "configuration": "Configuration", - "converter-configurations": "Converter configurations", - "token": "Security token", - "add-converter": "Add converter", - "add-config": "Add converter configuration", - "device-name-expression": "Device name expression", - "device-type-expression": "Device type expression", - "custom": "Custom", - "to-double": "To Double", - "transformer": "Transformer", - "json-required": "Transformer json is required.", - "json-parse": "Unable to parse transformer json.", - "attributes": "Attributes", - "add-attribute": "Add attribute", - "add-map": "Add mapping element", - "timeseries": "Timeseries", - "add-timeseries": "Add timeseries", - "field-required": "Field is required", - "brokers": "Brokers", - "add-broker": "Add broker", - "host": "Host", - "port": "Port", - "port-range": "Port should be in a range from 1 to 65535.", - "ssl": "Ssl", - "credentials": "Credentials", - "username": "Username", - "password": "Password", - "retry-interval": "Retry interval in milliseconds", - "sas": "Shared Access Signature", - "anonymous": "Anonymous", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA certificate file *", - "private-key": "Private key file *", - "cert": "Certificate file *", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "mapping": "Mapping", - "topic-filter": "Topic filter", - "converter-type": "Converter type", - "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in milliseconds", - "converter-json-required": "Converter json is required.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Connect requests", - "add-connect-request": "Add connect request", - "disconnect-requests": "Disconnect requests", - "add-disconnect-request": "Add disconnect request", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", - "add-device": "Add device", - "opc-server": "Servers", - "opc-add-server": "Add server", - "opc-add-server-prompt": "Please add server", - "opc-application-name": "Application name", - "opc-application-uri": "Application uri", - "opc-scan-period-in-seconds": "Scan period in seconds", - "opc-security": "Security", - "opc-identity": "Identity", - "opc-keystore": "Keystore", - "opc-type": "Type", - "opc-keystore-type": "Type", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Key password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Add server/slave", - "modbus-add-server-prompt": "Please add server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Automatically reconnect", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Serial port name", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits should be in a range from 7 to 8.", - "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Device name", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Attributes poll period (ms)", - "modbus-timeseries-poll-period": "Timeseries poll period (ms)", - "modbus-poll-period-range": "Poll period should be positive value.", - "modbus-tag": "Tag", - "modbus-function": "Function", - "modbus-register-address": "Register address", - "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Status", - "sync": "Sync", - "not-sync": "Not sync", - "last-sync-time": "Last sync time", - "not-available": "Not available" + "path": { + "path": "Kelias", + "path-decorator": "Kelio dekoratorius", + "decorator-symbol": "Dekoratoriaus simbolis", + "decorator-symbol-arrow-head": "Rodyklė", + "decorator-symbol-dash": "Brūkšnys", + "decorator-arrangement": "Dekoratoriaus išdėstymas", + "decorator-offset": "Pradžia", + "decorator-end-offset": "Pabaiga", + "decorator-repeat": "Kartojimas" + }, + "points": { + "points": "Taškai", + "point-tooltip": "Taško patarimas" }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", - "import-extensions": "Import extensions", - "import-extension": "Import extension", - "export-extension": "Export extension", - "file": "Extensions file", - "invalid-file-error": "Invalid extension file", - "text": "TEXT", - "json": "JSON", - "binary": "BINARY", - "hex": "HEX" - }, - "feature": { - "advanced-features": "Advanced features" - }, - "filter": { - "add": "Add filter", - "edit": "Edit filter", - "name": "Filter name", - "name-required": "Filter name is required.", - "duplicate-filter": "Filter with same name is already exists.", - "filters": "Filters", - "unable-delete-filter-title": "Unable to delete filter", - "unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "duplicate-filter-error": "Duplicate filter found '{{filter}}'.
    Filters must be unique within the dashboard.", - "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", - "filter": "Filter", - "editable": "Editable", - "no-filters-found": "No filters found.", - "no-filter-text": "No filter specified", - "add-filter-prompt": "Please add filter", - "no-filter-matching": "'{{filter}}' not found.", - "create-new-filter": "Create a new one!", - "create-new": "Create new", - "filter-required": "Filter is required.", - "operation": { - "operation": "Operation", - "equal": "equal", - "not-equal": "not equal", - "starts-with": "starts with", - "ends-with": "ends with", - "contains": "contains", - "not-contains": "not contains", - "greater": "greater than", - "less": "less than", - "greater-or-equal": "greater or equal", - "less-or-equal": "less or equal", - "and": "and", - "or": "or", - "in": "in", - "not-in": "not in" + "shape": { + "fill": "Užpildymas", + "fill-type-color": "Spalva", + "fill-type-stripe": "Dryžis", + "fill-type-image": "Paveikslėlis", + "color": "Spalva", + "stripe": "Dryžis", + "image": "Paveikslėlis", + "stroke": "Kontūras", + "fill-image": "Užpildo paveikslėlis", + "fill-image-type-image": "Paveikslėlis", + "fill-image-type-function": "Funkcija", + "preserve-aspect-ratio": "Išlaikyti proporcijas", + "opacity": "Nepermatomumas", + "angle": "Pasukimo kampas", + "scale": "Mastelis", + "fill-image-function": "Formos užpildo paveikslėlio funkcija", + "fill-images": "Formos užpildo paveikslėliai", + "stripe-pattern": "Dryžių raštas", + "first-stripe": "Pirmas dryžis", + "second-stripe": "Antras dryžis" }, - "ignore-case": "ignore case", - "value": "Value", - "remove-filter": "Remove filter", - "duplicate-filter-action": "Duplicate filter", - "preview": "Filter preview", - "no-filters": "No filters configured", - "add-filter": "Add filter", - "add-complex-filter": "Add complex filter", - "add-complex": "Add complex", - "complex-filter": "Complex filter", - "edit-complex-filter": "Edit complex filter", - "edit-filter-user-params": "Edit filter predicate user parameters", - "filter-user-params": "Filter predicate user parameters", - "user-parameters": "User parameters", - "display-label": "Label to display", - "order-priority": "Field order priority", - "key-filter": "Key filter", - "key-filters": "Key filters", - "key-name": "Key name", - "key-name-required": "Key name is required.", - "key-type": { - "key-type": "Key type", - "attribute": "Attribute", - "timeseries": "Timeseries", - "entity-field": "Entity field", - "constant": "Constant" + "polygon": { + "polygon-key": "Daugiakampio raktas", + "polygon-key-required": "Daugiakampio raktas yra privalomas", + "no-polygons": "Daugiakampiai nesukonfigūruoti", + "add-polygon": "Pridėti daugiakampį", + "polygon-configuration": "Daugiakampio konfigūracija", + "remove-polygon": "Pašalinti daugiakampį", + "edit": "Redaguoti daugiakampį", + "remove-polygon-for": "Pašalinti daugiakampį objektui '{{entityName}}'", + "cut": "Iškirpti daugiakampio sritį", + "rotate": "Pasukti daugiakampį", + "draw-rectangle": "Nubrėžti stačiakampį", + "draw-polygon": "Nubrėžti daugiakampį", + "polygon-place-first-point-cut-hint": "Spustelėkite, kad padėtumėte pirmą tašką", + "continue-polygon-cut-hint": "Spustelėkite, kad tęstumėte braižymą", + "finish-polygon-cut-hint": "Spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "polygon-place-first-point-hint": "Daugiakampis: spustelėkite, kad padėtumėte pirmą tašką", + "polygon-place-first-point-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "continue-polygon-hint": "Daugiakampis: spustelėkite, kad tęstumėte braižymą", + "continue-polygon-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad tęstumėte braižymą", + "finish-polygon-hint": "Daugiakampis: spustelėkite pirmą žymeklį, kad baigtumėte braižymą", + "finish-polygon-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "rectangle-place-first-point-hint": "Stačiakampis: spustelėkite, kad padėtumėte pirmą tašką", + "rectangle-place-first-point-hint-with-entity": "Stačiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "finish-rectangle-hint": "Stačiakampis: spustelėkite, kad baigtumėte braižymą", + "finish-rectangle-hint-with-entity": "Stačiakampis objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte" }, - "value-type": { - "value-type": "Value type", - "string": "String", - "numeric": "Numeric", - "boolean": "Boolean", - "date-time": "Datetime" + "circle": { + "circle-key": "Apskritimo raktas", + "circle-key-required": "Apskritimo raktas yra privalomas", + "no-circles": "Apskritimai nesukonfigūruoti", + "add-circle": "Pridėti apskritimą", + "circle-configuration": "Apskritimo konfigūracija", + "remove-circle": "Pašalinti apskritimą", + "edit": "Redaguoti apskritimą", + "remove-circle-for": "Pašalinti apskritimą objektui '{{entityName}}'", + "draw-circle": "Nubrėžti apskritimą", + "place-circle-center-hint-with-entity": "Apskritimas objektui '{{entityName}}': spustelėkite, kad padėtumėte apskritimo centrą", + "place-circle-center-hint": "Apskritimas: spustelėkite, kad padėtumėte apskritimo centrą", + "finish-circle-hint-with-entity": "Apskritimas objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte", + "finish-circle-hint": "Apskritimas: spustelėkite, kad baigtumėte braižymą" }, - "value-type-required": "Key value type is required.", - "key-value-type-change-title": "Are you sure you want to change key value type?", - "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.", - "no-key-filters": "No key filters configured", - "add-key-filter": "Add key filter", - "remove-key-filter": "Remove key filter", - "edit-key-filter": "Edit key filter", - "date": "Date", - "time": "Time", - "current-tenant": "Current tenant", - "current-customer": "Current customer", - "current-user": "Current user", - "current-device": "Current device", - "default-value": "Default value", - "dynamic-source-type": "Dynamic source type", - "dynamic-value": "Dynamic value", - "no-dynamic-value": "No dynamic value", - "source-attribute": "Source attribute", - "switch-to-dynamic-value": "Switch to dynamic value", - "switch-to-default-value": "Switch to default value", - "inherit-owner": "Inherit from owner", - "source-attribute-not-set": "If source attribute isn't set" - }, - "fullscreen": { - "expand": "Rodyti per visą ekraną", - "exit": "Išjungti rodymą per visą ekraną", - "toggle": "Rodyti per visą ekraną", - "fullscreen": "Rodyti per visą ekraną" - }, - "function": { - "function": "Funkcija" + "select-entity": "Pasirinkti objektą", + "select-entity-hint": "Patarimas: po pasirinkimo spustelėkite žemėlapyje, kad nustatytumėte padėtį" + }, + "select-entity": "Pasirinkti objektą", + "select-entity-hint": "Patarimas: po pasirinkimo spustelėkite žemėlapyje, kad nustatytumėte padėtį", + "tooltips": { + "placeMarker": "Spustelėkite, kad padėtumėte objektą '{{entityName}}'", + "firstVertex": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "firstVertex-cut": "Spustelėkite, kad padėtumėte pirmą tašką", + "continueLine": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad tęstumėte braižymą", + "continueLine-cut": "Spustelėkite, kad tęstumėte braižymą", + "finishLine": "Spustelėkite bet kurį esamą žymeklį, kad baigtumėte", + "finishPoly": "Daugiakampis objektui '{{entityName}}': spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "finishPoly-cut": "Spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "finishRect": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte", + "startCircle": "Apskritimas objektui '{{entityName}}': spustelėkite, kad padėtumėte apskritimo centrą", + "finishCircle": "Apskritimas objektui '{{entityName}}': spustelėkite, kad baigtumėte apskritimą", + "placeCircleMarker": "Spustelėkite, kad padėtumėte apskritimo žymeklį" + }, + "actions": { + "finish": "Baigti", + "cancel": "Atšaukti", + "removeLastVertex": "Pašalinti paskutinį tašką" + }, + "buttonTitles": { + "drawMarkerButton": "Padėti objektą", + "drawPolyButton": "Sukurti daugiakampį", + "drawLineButton": "Sukurti liniją", + "drawCircleButton": "Sukurti apskritimą", + "drawRectButton": "Sukurti stačiakampį", + "editButton": "Redagavimo režimas", + "dragButton": "Vilkimo režimas", + "cutButton": "Iškirpti daugiakampio sritį", + "deleteButton": "Pašalinti", + "drawCircleMarkerButton": "Sukurti apskritimo žymeklį", + "rotateButton": "Pasukti daugiakampį" + }, + "map-provider-settings": "Žemėlapio tiekėjo nustatymai", + "map-provider": "Žemėlapio tiekėjas", + "map-provider-google": "Google žemėlapiai", + "map-provider-openstreet": "OpenStreet žemėlapiai", + "map-provider-here": "HERE žemėlapiai", + "map-provider-image": "Paveikslėlio žemėlapis", + "map-provider-tencent": "Tencent žemėlapiai", + "openstreet-provider": "OpenStreet žemėlapių tiekėjas", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (numatytasis)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Naudoti pasirinktinį tiekėją", + "custom-provider-tile-url": "Pasirinktinio tiekėjo plytelių URL", + "google-maps-api-key": "Google Maps API raktas", + "default-map-type": "Numatytasis žemėlapio tipas", + "google-map-type-roadmap": "Keliai", + "google-map-type-satelite": "Palydovas", + "google-map-type-hybrid": "Hibridinis", + "google-map-type-terrain": "Reljefas", + "map-layer": "Žemėlapio sluoksnis", + "here-map-normal-day": "HERE.normalDay (numatytasis)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Prisijungimo duomenys", + "here-app-id": "HERE programos ID", + "here-app-code": "HERE programos kodas", + "here-api-key": "HERE API raktas", + "here-use-new-version-api-3": "Naudoti API 3 versiją", + "tencent-maps-api-key": "Tencent Maps API raktas", + "tencent-map-type-roadmap": "Keliai", + "tencent-map-type-satelite": "Palydovas", + "tencent-map-type-hybrid": "Hibridinis", + "image-map-background": "Paveikslėlio žemėlapio fonas", + "image-map-background-from-entity-attribute": "Naudoti paveikslėlį iš objekto atributo kaip žemėlapio foną", + "image-url-source-entity-alias": "Paveikslėlio URL šaltinio objekto pseudonimas", + "image-url-source-entity-attribute": "Paveikslėlio URL šaltinio objekto atributas", + "common-map-settings": "Bendrieji žemėlapio nustatymai", + "x-pos-key-name": "X padėties rakto pavadinimas", + "y-pos-key-name": "Y padėties rakto pavadinimas", + "latitude-key-name": "Platumos rakto pavadinimas", + "longitude-key-name": "Ilgumos rakto pavadinimas", + "default-map-zoom-level": "Numatytasis priartinimo lygis (0–20)", + "default-map-center-position": "Numatytoji žemėlapio centro padėtis (0,0)", + "disable-scroll-zooming": "Išjungti priartinimą slenkant", + "disable-double-click-zooming": "Išjungti priartinimą dukart spustelėjus", + "disable-zoom-control-buttons": "Išjungti priartinimo valdymo mygtukus", + "fit-map-bounds": "Pritaikyti žemėlapio ribas, kad apimtų visus žymeklius", + "use-default-map-center-position": "Naudoti numatytąją žemėlapio centro padėtį", + "entities-limit": "Objektų įkėlimo riba", + "markers-settings": "Žymeklių nustatymai", + "marker-offset-x": "Žymeklio X poslinkis, lyginant su padėtimi (daugiklis žymeklio pločiui)", + "marker-offset-y": "Žymeklio Y poslinkis, lyginant su padėtimi (daugiklis žymeklio aukščiui)", + "position-function": "Padėties konversijos funkcija, grąžinanti x,y koordinates nuo 0 iki 1", + "draggable-marker": "Vilkiojamas žymeklis", + "label": "Etiketė", + "show-label": "Rodyti etiketę", + "use-label-function": "Naudoti etiketės funkciją", + "label-pattern": "Etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "label-function": "Etiketės funkcija", + "tooltip": "Patarimas", + "show-tooltip": "Rodyti patarimą", + "show-tooltip-action": "Veiksmas, kai rodomas patarimas", + "show-tooltip-action-click": "Rodyti spustelėjus (numatytasis)", + "show-tooltip-action-hover": "Rodyti užvedus žymeklį", + "auto-close-tooltips": "Automatiškai uždaryti patarimus", + "use-tooltip-function": "Naudoti patarimo funkciją", + "tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "tooltip-function": "Patarimo funkcija", + "tooltip-offset-x": "Patarimo X poslinkis (pagal žymeklio inkarą, dauginamas iš pločio)", + "tooltip-offset-y": "Patarimo Y poslinkis (pagal žymeklio inkarą, dauginamas iš aukščio)", + "color": "Spalva", + "use-color-function": "Naudoti spalvos funkciją", + "color-function": "Spalvos funkcija", + "marker-image": "Žymeklio paveikslėlis", + "use-marker-image-function": "Naudoti žymeklio paveikslėlio funkciją", + "custom-marker-image": "Pasirinktinis žymeklio paveikslėlis", + "custom-marker-image-size": "Pasirinktinio žymeklio paveikslėlio dydis (px)", + "marker-image-function": "Žymeklio paveikslėlio funkcija", + "marker-images": "Žymeklio paveikslėliai", + "polygon-settings": "Daugiakampio nustatymai", + "show-polygon": "Rodyti daugiakampį", + "polygon-key-name": "Daugiakampio rakto pavadinimas", + "enable-polygon-edit": "Įjungti daugiakampio redagavimą", + "polygon-label": "Daugiakampio etiketė", + "show-polygon-label": "Rodyti daugiakampio etiketę", + "use-polygon-label-function": "Naudoti daugiakampio etiketės funkciją", + "polygon-label-pattern": "Daugiakampio etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "polygon-label-function": "Daugiakampio etiketės funkcija", + "polygon-tooltip": "Daugiakampio patarimas", + "show-polygon-tooltip": "Rodyti daugiakampio patarimą", + "auto-close-polygon-tooltips": "Automatiškai uždaryti daugiakampio patarimus", + "use-polygon-tooltip-function": "Naudoti daugiakampio patarimo funkciją", + "polygon-tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "polygon-tooltip-function": "Daugiakampio patarimo funkcija", + "polygon-color": "Daugiakampio spalva", + "polygon-opacity": "Daugiakampio permatomumas", + "use-polygon-color-function": "Naudoti daugiakampio spalvos funkciją", + "polygon-color-function": "Daugiakampio spalvos funkcija", + "polygon-stroke": "Daugiakampio brūkšnys", + "stroke-color": "Brūkšnio spalva", + "stroke-opacity": "Brūkšnio permatomumas", + "stroke-weight": "Brūkšnio storis", + "use-polygon-stroke-color-function": "Naudoti daugiakampio brūkšnio spalvos funkciją", + "polygon-stroke-color-function": "Daugiakampio brūkšnio spalvos funkcija", + "circle-settings": "Apskritimo nustatymai", + "show-circle": "Rodyti apskritimą", + "circle-key-name": "Apskritimo rakto pavadinimas", + "enable-circle-edit": "Įjungti apskritimo redagavimą", + "circle-label": "Apskritimo etiketė", + "show-circle-label": "Rodyti apskritimo etiketę", + "use-circle-label-function": "Naudoti apskritimo etiketės funkciją", + "circle-label-pattern": "Apskritimo etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "circle-label-function": "Apskritimo etiketės funkcija", + "circle-tooltip": "Apskritimo patarimas", + "show-circle-tooltip": "Rodyti apskritimo patarimą", + "auto-close-circle-tooltips": "Automatiškai uždaryti apskritimo patarimus", + "use-circle-tooltip-function": "Naudoti apskritimo patarimo funkciją", + "circle-tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "circle-tooltip-function": "Apskritimo patarimo funkcija", + "circle-fill-color": "Apskritimo užpildo spalva", + "circle-fill-color-opacity": "Apskritimo užpildo permatomumas", + "use-circle-fill-color-function": "Naudoti apskritimo užpildo spalvos funkciją", + "circle-fill-color-function": "Apskritimo užpildo spalvos funkcija", + "circle-stroke": "Apskritimo brūkšnys", + "use-circle-stroke-color-function": "Naudoti apskritimo brūkšnio spalvos funkciją", + "circle-stroke-color-function": "Apskritimo brūkšnio spalvos funkcija", + "markers-clustering-settings": "Žymeklių grupavimo nustatymai", + "use-map-markers-clustering": "Naudoti žymeklių grupavimą", + "zoom-on-cluster-click": "Priartinti paspaudus ant grupės", + "max-cluster-zoom": "Didžiausias priartinimo lygis, kuriame žymeklis gali priklausyti grupei (0–18)", + "max-cluster-radius-pixels": "Didžiausias grupės spindulys (pikseliais)", + "cluster-zoom-animation": "Rodyti animaciją žymekliams priartinant", + "show-markers-bounds-on-cluster-mouse-over": "Rodyti žymeklių ribas užvedus pelę ant grupės", + "spiderfy-max-zoom-level": "Išskleisti (spiderfy) didžiausiame priartinimo lygyje, kad matytųsi visi žymekliai", + "load-optimization": "Įkėlimo optimizavimas", + "cluster-chunked-loading": "Naudoti dalinį žymeklių įkėlimą, kad puslapis neužstrigtų", + "cluster-markers-lazy-load": "Naudoti „lazy load“ žymeklių įkėlimui", + "editor-settings": "Redaktoriaus nustatymai", + "enable-snapping": "Įjungti pritraukimą prie kitų viršūnių tikslumui", + "init-draggable-mode": "Paleisti žemėlapį vilkiojimo režimu", + "hide-all-edit-buttons": "Slėpti visus redagavimo valdiklius", + "hide-draw-buttons": "Slėpti braižymo mygtukus", + "hide-edit-buttons": "Slėpti redagavimo mygtukus", + "hide-remove-button": "Slėpti šalinimo mygtuką", + "route-map-settings": "Maršruto žemėlapio nustatymai", + "trip-animation-settings": "Kelionės animacijos nustatymai", + "normalization-step": "Normalizavimo žingsnis (ms)", + "tooltip-background-color": "Patarimo fono spalva", + "tooltip-font-color": "Patarimo teksto spalva", + "tooltip-opacity": "Patarimo permatomumas (0–1)", + "auto-close-tooltip": "Automatiškai uždaryti patarimą", + "rotation-angle": "Papildomas žymeklio pasukimo kampas (laipsniais)", + "path-settings": "Kelio nustatymai", + "path-color": "Kelio spalva", + "use-path-color-function": "Naudoti kelio spalvos funkciją", + "path-color-function": "Kelio spalvos funkcija", + "path-decorator": "Kelio dekoratorius", + "use-path-decorator": "Naudoti kelio dekoratorių", + "decorator-symbol": "Dekoratoriaus simbolis", + "decorator-symbol-arrow-head": "Rodyklė", + "decorator-symbol-dash": "Brūkšnys", + "decorator-symbol-size": "Dekoratoriaus simbolio dydis (px)", + "use-path-decorator-custom-color": "Naudoti pasirinktinę dekoratoriaus spalvą", + "decorator-custom-color": "Dekoratoriaus pasirinktinė spalva", + "decorator-offset": "Dekoratoriaus poslinkis", + "end-decorator-offset": "Dekoratoriaus pabaigos poslinkis", + "decorator-repeat": "Dekoratoriaus kartojimas", + "points-settings": "Taškų nustatymai", + "show-points": "Rodyti taškus", + "point-color": "Taško spalva", + "point-size": "Taško dydis (px)", + "use-point-color-function": "Naudoti taško spalvos funkciją", + "point-color-function": "Taško spalvos funkcija", + "use-point-as-anchor": "Naudoti tašką kaip inkarą", + "point-as-anchor-function": "Taško kaip inkaro funkcija", + "independent-point-tooltip": "Nepriklausomas taško patarimas", + "clustering-markers": "Žymeklių grupavimas", + "use-icon-create-function": "Naudoti žymeklių spalvos funkciją", + "marker-color-function": "Žymeklio spalvos funkcija" }, - "gateway": { - "gateway-exists": "Device with same name is already exists.", - "gateway-name": "Gateway name", - "gateway-name-required": "Gateway name is required.", - "gateway-saved": "Gateway configuration successfully saved.", - "gateway": "Gateway", - "create-new-gateway": "Create a new gateway", - "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", - "no-gateway-found": "No gateway found.", - "no-gateway-matching": " '{{item}}' not found." - }, - "grid": { - "delete-item-title": "Ar tikrai norite pašalinti šį elementą?", - "delete-item-text": "Būkite dėmesingi, po patvirtinimo šio elemento ir visos su juo susijusios informacijos atkurti nebegalėsite.", - "delete-items-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 elementą} other {# elementus} }?", - "delete-items-action-title": "Panaikinti { count, plural, =1 {1 elementą} other {# elementus} }", - "delete-items-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti elementai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", - "add-item-text": "Pridėti naują elementą", - "no-items-text": "Elementų nėra", - "item-details": "Informacija apie elementą", - "delete-item": "Panaikinti elementą", - "delete-items": "Panaikinti elmentus", - "scroll-to-top": "Slinkti į viršų" - }, - "help": { - "goto-help-page": "Pagalba", - "show-help": "Pagalba" + "markdown": { + "use-markdown-text-function": "Naudoti markdown/HTML reikšmės funkciją", + "markdown-text-function": "Markdown/HTML reikšmės funkcija", + "markdown-text-pattern": "Markdown/HTML šablonas (markdown arba HTML su kintamaisiais, pvz.: '${entityName} arba ${keyName} - kažkoks tekstas.')", + "apply-default-markdown-style": "Taikyti numatytąjį markdown stilių", + "markdown-css": "Markdown/HTML CSS" }, - "home": { - "home": "Pagrindinis", - "profile": "Profilis", - "logout": "Atsijungti", - "menu": "Meniu", - "avatar": "Paveiksliukas", - "open-user-menu": "Atverti vartoto meniu" - }, - "file-input": { - "browse-file": "Pasirinkti failą", - "browse-files": "Pasirinkti failus" - }, - "image-input": { - "drop-images-or": "Užvilkite paveikslėlį arba", - "drag-and-drop": "Užvilkite", - "or": "arba", - "browse": "Pasirinkite", - "no-images": "Paveikslėlis nepasirinktas", - "images": "paveikslėliai" - }, - "import": { - "no-file": "Nepasirinktas failas", - "drop-file": "Nuvilkite JSON failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-csv-file": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-json-file-or": "Nuvilkite JSON failą arba", - "drop-file-csv": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-file-csv-or": "Nuvilkite CSV failą arba", - "column-value": "Reikšmė", - "column-title": "Pavadinimas", - "column-example": "Duomenų pavyzdys", - "column-key": "Atributo/telemetrijos raktas", - "credentials": "Įgaliojimai", - "csv-delimiter": "CSV skyriklis", - "csv-first-line-header": "Pirmoje eilutėje stulpelių pavadinimai", - "csv-update-data": "Atnaujinti artibutą/telemetriją", - "details": "Informacija", - "import-csv-number-columns-error": "Faile turi būti bent du stulpeliai", - "import-csv-invalid-format-error": "Neteisingas failo formatas. Elutė: '{{line}}'", - "column-type": { - "name": "Pavadinimas", - "type": "Tipas", - "label": "Etiketė", - "column-type": "Stulpelio tipas", - "client-attribute": "Kliento atributas", - "shared-attribute": "Bendrinamas atributas", - "server-attribute": "Serverio atributas", - "timeseries": "Telemetrija", - "entity-field": "Subjekto laukas", - "access-token": "Prieigos raktas", - "x509": "X.509", - "mqtt": { - "client-id": "MQTT client ID", - "user-name": "MQTT user name", - "password": "MQTT password" - }, - "lwm2m": { - "client-endpoint": "LwM2M endpoint client name", - "security-config-mode": "LwM2M security config mode", - "client-identity": "LwM2M client identity", - "client-key": "LwM2M client key", - "client-cert": "LwM2M client public key", - "bootstrap-server-security-mode": "LwM2M bootstrap server security mode", - "bootstrap-server-secret-key": "LwM2M bootstrap server secret key", - "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id", - "lwm2m-server-security-mode": "LwM2M server security mode", - "lwm2m-server-secret-key": "LwM2M server secret key", - "lwm2m-server-public-key-id": "LwM2M server public key or id" - }, - "snmp": { - "host": "SNMP host", - "port": "SNMP port", - "version": "SNMP version (v1, v2c or v3)", - "community-string": "SNMP community string" - }, - "isgateway": "Is Gateway", - "activity-time-from-gateway-device": "Activity time from gateway device", - "description": "Description", - "edge-license-key": "License Key", - "cloud-endpoint": "Cloud Endpoint", - "routing-key": "Edge key", - "secret": "Edge secret" - }, - "stepper-text": { - "select-file": "Pasirinkite failą", - "configuration": "Importo konfigūracija", - "column-type": "Pasirinkite stulpelių tipus", - "creat-entities": "Sukurti naujus subjektus" - }, - "message": { - "create-entities": "Sukurti {{count}} nauji subjektai.", - "update-entities": "Atnaujinti {{count}} subjektai.", - "error-entities": "Kuriant {{count}} subjektų įvyko klaidų." - } + "simple-card": { + "label": "Etiketė", + "label-position": "Etiketės padėtis", + "label-position-left": "Kairėje", + "label-position-top": "Viršuje" }, - "integration": { - "integration": "Integration", - "integrations": "Integrations", - "integrations-center": "Integrations center", - "select-integration": "Select integration", - "no-integrations-matching": "No integrations matching '{{entity}}' were found.", - "integration-required": "Integration is required", - "delete": "Delete integration", - "management": "Integrations management", - "add-integration-text": "Add new integration", - "no-integrations-text": "No integrations found", - "data-convertor-name": "{{convertorType}} data converter for {{integrationName}}", - "integration-name": "{{integrationType}} integration", - "selected-integrations": "{ count, plural, =1 {1 integration} other {# integrations} } selected", - "delete-integration-title": "Are you sure you want to delete the integration '{{integrationName}}'?", - "delete-integration-text": "Be careful, after the confirmation the integration and all related data will become unrecoverable.", - "delete-integrations-title": "Are you sure you want to delete { count, plural, =1 {1 integration} other {# integrations} }?", - "delete-integrations-action-title": "Delete { count, plural, =1 {1 integration} other {# integrations} }", - "delete-integrations-text": "Be careful, after the confirmation all selected integrations will be removed and all related data will become unrecoverable.", - "events": "Events", - "enabled": "Enable integration", - "allow-create-devices-or-assets": "Allow create devices or assets", - "all-types": "All integration types", - "add": "Add Integration", - "search": "Search integrations", - "integration-details": "Integration details", - "details": "Details", - "copyId": "Copy integration Id", - "idCopiedMessage": "Integration Id has been copied to clipboard", - "debug-mode": "Debug mode", - "enable-security": "Enable security", - "enable-security-new": "Enable security for automatic token updates", - "headers-filter": "Headers filter", - "header": "Header", - "no-headers-filter": "No headers filter", - "downlink-url": "Downlink URL", - "downlink-url-required": "Downlink URL is required", - "create-loriot-output": "Create Loriot Application output", - "send-downlink": "Send downlink", - "allow-downlink": "Allow downlink", - "server": "Server", - "server-required": "Server is required", - "domain": "Domain", - "app-id": "Application ID", - "app-id-required": "Application ID is required", - "app-token": "Application Access Token", - "app-token-required": "Application Access Token is required", - "email": "Email", - "email-required": "Email is required", - "application-uri": "Application URI", - "as-id": "AS ID", - "as-id-required": "AS ID is required.", - "as-key": "AS Key", - "as-key-required": "AS Key is required.", - "client-id-new": "Client Id", - "client-id-new-required": "Client Id (login) is required (login).", - "client-secret": "Client Secret", - "client-secret-required": "Client Secret (password) is required (password).", - "max-time-diff-in-seconds": "Maximum time difference (seconds)", - "max-time-diff-in-seconds-required": "Maximum time difference is required.", - "created-time": "Created time", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "base-url": "Base URL", - "base-url-required": "Base URL is required", - "security-key": "Security key", - "http-endpoint": "HTTP endpoint URL", - "replace-no-content-to-ok": "Replace response status from 'No-Content' to 'OK'", - "copy-http-endpoint-url": "Copy HTTP endpoint URL", - "http-endpoint-url-copied-message": "HTTP endpoint URL has been copied to clipboard", - "host": "Host", - "host-required": "Host is required.", - "host-private": "Host must be in the public network.", - "url-private": "URL must be in the public network.", - "host-type": "Host type", - "host-type-required": "Host type is required.", - "api-version": "Use API v3", - "custom-host": "Custom host", - "custom-host-required": "Custom host is required.", - "port": "Port", - "port-required": "Port is required.", - "port-range": "Port should be in a range from 1 to 65535.", - "connect-timeout": "Connection timeout (sec)", - "connect-timeout-required": "Connection timeout is required.", - "connect-timeout-range": "Connection timeout should be in a range from 1 to 200.", - "client-id": "Client ID", - "client-id-required": "Client ID is required.", - "client-id-hint": "Hint: Optional. Leave empty for auto-generated client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same client ID. To connect to such brokers, your MQTT client ID must be unique.", - "max-bytes-in-message": "Max bytes in message", - "max-bytes-in-message-range": "Max bytes in message should be in a range from 1 to 256000000.", - "device-id": "Device ID", - "device-id-required": "Device ID is required.", - "device-id-range": "Device ID should be in a range from 1 to 65535 characters.", - "device-id-pattern": "Device ID should consist of numbers, upper and lower case letters. [MQTT-3.1.3-5]", - "group-id": "Group ID", - "group-id-required": "Group ID is required.", - "topics": "Topics", - "topics-required": "Topic is required.", - "routing-keys": "Routing keys", - "routing-keys-required": "Routing key is required.", - "queues": "Queues", - "queues-required": "Queue name is required.", - "durable": "Durable", - "exclusive": "Exclusive", - "autoDelete": "Auto delete", - "exchange-name": "Exchange name", - "exchange-name-required": "Exchange name is required.", - "downlink-topic": "Downlink topic", - "connection-timeout": "Connection timeout, ms", - "connection-timeout-min": "Invalid connection timeout value.", - "handshake-timeout": "Handshake timeout, ms", - "handshake-timeout-min": "Invalid handshake timeout value.", - "virtual-host": "Virtual host", - "rabbit-mq-poll-interval": "Poll interval, ms", - "rabbit-mq-poll-interval-min": "Invalid poll interval value.", - "application-server-url": "Application server URL", - "application-server-url-required": "Application server URL is required.", - "application-server-api-token": "Application server API Token", - "application-server-api-token-required": "Application server API Token is required.", - "use-api-four-plus": "Use API for ChirpStack 4+", - "bootstrap-servers": "Bootstrap servers", - "bootstrap-servers-required": "Bootstrap servers is required.", - "poll-interval": "Poll interval", - "poll-interval-required": "Poll interval is required.", - "poll-interval-min-value": "Poll interval value can't be less then 1", - "auto-create-topics": "Auto create topics", - "clean-session": "Clean session", - "enable-ssl": "Enable SSL", - "credentials": "Credentials", - "credentials-type": "Credentials type", - "credentials-type-required": "Credentials type is required.", - "username": "Username", - "username-required": "Username is required.", - "password": "Password", - "password-required": "Password is required.", - "azure-ca-cert": "CA certificate file", - "ca-cert": "CA certificate file", - "private-key": "Private key file", - "private-key-password": "Private key password", - "cert": "Certificate file", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "drop-file-or": "Drag and drop a file or", - "check-connection": "Check connection", - "check-success": "Connection established.", - "topic-filters": "Topic filters", - "remove-topic-filter": "Remove topic filter", - "add-topic-filter": "Add topic filter", - "add-topic-filter-prompt": "Please add topic filter", - "topic": "Topic", - "mqtt-qos": "QoS", - "mqtt-qos-required": "QoS is required", - "mqtt-qos-range": "QoS values range is from 0 to 2", - "mqtt-qos-at-most-once": "At most once", - "mqtt-qos-at-least-once": "At least once", - "mqtt-qos-exactly-once": "Exactly once", - "downlink-topic-pattern": "Downlink topic pattern", - "downlink-topic-pattern-required": "Downlink topic pattern is required.", - "retained-message": "Retained", - "aws-access-key-id": "AWS access key Id", - "aws-secret-access-key": "AWS secret access key", - "aws-region": "Region", - "aws-iot-endpoint": "AWS IoT Endpoint", - "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", - "aws-iot-credentials": "AWS IoT Credentials", - "aws-sqs-polling-period-in-seconds": "Polling period in seconds", - "aws-sqs-queue-url": "SQS Queue URL", - "aws-sqs-queue-url-required": "SQS Queue URL is required", - "aws-sqs-access-key-id-required": "Access Key Id is required", - "aws-sqs-secret-access-key-required": "Secret Access Key is required", - "application-credentials": "Application Credentials", - "api-key": "API Key", - "api-key-required": "API Key is required.", - "api-key-format": "Invalid API Key format.", - "auth-token": "Authentication Token", - "auth-token-required": "Authentication Token is required.", - "region": "Region", - "region-required": "Region is required.", - "application-id": "Application ID", - "application-id-required": "Application ID is required.", - "access-id": "Access Id", - "access-id-required": "Access Id is required.", - "access-key": "Access Key", - "access-key-required": "Access Key is required.", - "access-key-min-length": "Access Key is too short.", - "access-key-max-length": "Access Key is too long.", - "connection-parameters": "Connection parameters", - "service-bus-namespace-name": "Service Bus Namespace Name", - "service-bus-namespace-name-required": "Service Bus Namespace Name is required.", - "connection-string": "Connection string", - "consumer-group": "Consumer group", - "connection-string-required": "Connection string required!", - "event-hub-name": "Event Hub Name", - "event-hub-name-required": "Event Hub Name is required.", - "event-iot-hub-name-required": "Iot Hub Name is required for downlink", - "sas-key-name": "SAS Key Name", - "sas-key-name-required": "SAS Key Name is required.", - "sas-key": "SAS Key", - "sas-key-required": "SAS Key is required.", - "iot-hub-name": "IoT Hub Name (required for downlink)", - "hostname": "Hostname", - "hostname-required": "Hostname is required", - "integration-clazz": "Integration class", - "integration-clazz-required": "Integration class is required", - "integration-configuration": "Integration JSON configuration", - "metadata": "Metadata", - "type": "Integration type", - "select-integration-type": "Select integration type", - "type-required": "Integration type is required.", - "type-not-found": "Integration type not found", - "uplink-converter": "Uplink data converter", - "uplink-converter-required": "Uplink data converter is required.", - "downlink-converter": "Downlink data converter", - "type-http": "HTTP", - "type-http-description": "Transferring text or multimedia files protocol.", - "type-ocean-connect": "OceanConnect", - "type-ocean-connect-description": "Integration of devices with different capabilities with the IoT platform.", - "type-sigfox": "SigFox", - "type-sigfox-description": "Lightweight protocol to handle small data transmissions.", - "type-thingpark": "ThingPark", - "type-thingpark-description": "Message-oriented transport layer protocol.", - "type-loriot": "Loriot", - "type-loriot-description": "Distributed LoRaWAN infrastructure.", - "type-thingpark-enterprise": "ThingParkEnterprise", - "type-thingpark-enterprise-description": "Messaging platform for the LPWA network.", - "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", - "type-tmobile-iot-cdp-description": "Mobile IoT connectivity service.", - "type-mqtt": "MQTT", - "type-mqtt-description": "Machine to machine network protocol.", - "type-aws-iot": "AWS IoT", - "type-aws-iot-description": "Management, storage, and analytics service.", - "type-aws-sqs": "AWS SQS", - "type-aws-sqs-description": "Fully managed message queuing service.", - "type-aws-kinesis": "AWS Kinesis", - "type-aws-kinesis-description": "Service for real-time video and data streams.", - "type-ibm-watson-iot": "IBM Watson IoT", - "type-ibm-watson-iot-description": "Fully managed, cloud-hosted service.", - "type-ttn": "The Things Stack Community", - "type-ttn-description": "Open and decentralized LoRaWAN network.", - "type-tti": "The Things Stack Industries", - "type-tti-description": "Software to operate private LoRaWAN networks at scale.", - "type-chirpstack": "ChirpStack", - "type-chirpstack-description": "LoRaWAN network configuration server.", - "type-particle": "Particle", - "type-particle-description": "IoT enablement platform-as-a-service.", - "type-azure-event-hub": "Azure Event Hub", - "type-azure-event-hub-description": "Big data streaming platform and event ingestion service.", - "type-azure-iot-hub": "Azure IoT Hub", - "type-azure-iot-hub-description": "Cloud-hosted messaging service.", - "type-opc-ua": "OPC-UA", - "type-opc-ua-description": "Cross-platform for data exchange from sensors to cloud apps.", - "type-custom": "Custom", - "type-coap": "CoAP", - "type-coap-description": "Machine to machine network protocol for constrained devices.", - "type-udp": "UDP", - "type-udp-description": "Message-oriented transport layer protocol.", - "type-tcp": "TCP", - "type-tcp-description": "Connection-oriented protocol.", - "type-kafka": "Kafka", - "type-kafka-description": "Streaming analytics, data integration streaming platform.", - "type-rabbitmq": "RabbitMQ", - "type-rabbitmq-description": "Message broker that supports multiple messaging protocols.", - "type-pubsub": "Pub/Sub", - "type-pubsub-description": "Service for asynchronous messaging between apps.", - "type-apache-pulsar": "Apache Pulsar", - "type-apache-pulsar-description": "Distributed pub-sub messaging and streaming platform.", - "type-tuya": "Tuya", - "type-tuya-description": "Cloud-hosted messaging. device platform.", - "type-azure-service-bus": "Azure Service Bus", - "type-azure-service-bus-description": "Fully managed enterprise message broker with message queues and publish-subscribe topics.", - "opc-ua-application-name": "Application name", - "opc-ua-application-uri": "Application uri", - "opc-ua-scan-period-in-seconds": "Scan period in seconds", - "opc-ua-scan-period-in-seconds-required": "Scan period is required", - "opc-ua-timeout": "Timeout in milliseconds", - "opc-ua-timeout-required": "Timeout is required", - "opc-ua-security": "Security", - "opc-ua-security-required": "Security is required", - "opc-ua-identity": "Identity", - "opc-ua-identity-required": "Identity is required", - "opc-ua-keystore": "Keystore", - "add-opc-ua-keystore-prompt": "Please add keystore file", - "opc-ua-keystore-required": "Keystore is required", - "opc-ua-type": "Type", - "opc-ua-keystore-type": "Type", - "opc-ua-keystore-type-required": "Type is required", - "opc-ua-keystore-location": "Location *", - "opc-ua-keystore-password": "Password", - "opc-ua-keystore-password-required": "Password is required", - "opc-ua-keystore-alias": "Alias", - "opc-ua-keystore-alias-required": "Alias is required", - "opc-ua-keystore-key-password": "Key password", - "opc-ua-keystore-key-password-required": "Key password is required", - "opc-ua-mapping": "Mapping", - "add-opc-ua-mapping-prompt": "Please add mapping", - "opc-ua-mapping-type": "Mapping type", - "opc-ua-mapping-type-required": "Mapping type is required", - "opc-ua-device-node-pattern": "Device Node Pattern", - "opc-ua-device-node-pattern-required": "Device Node Pattern is required", - "opc-ua-namespace": "Namespace", - "opc-ua-add-map": "Add mapping element", - "kinesis-stream-name": "Stream name", - "kinesis-stream-name-required": "Stream name is required", - "kinesis-region": "Region", - "kinesis-region-required": "Region is required", - "kinesis-access-key-id": "Access Key Id", - "kinesis-access-key-id-required": "Access Key Id is required", - "kinesis-secret-access-key": "Secret Access Key", - "kinesis-secret-access-key-required": "Secret Access Key is required", - "kinesis-use-consumers-with-enhanced-fan-out": "Use Consumers with Enhanced Fan-Out", - "kinesis-use-credentials-from-instance-metadata": "Use credentials from the Amazon EC2 Instance Metadata Service", - "kinesis-application-name": "Application name (by default equals Stream name)", - "kinesis-initial-position-in-stream": "Initial position in stream", - "kinesis-initial-position-in-stream-required": "Initial position in stream required", - "kinesis-max-records": "Max records", - "kinesis-max-records-required": "Max records is required", - "kinesis-max-records-length-range": "Max records length should be in a range from 1 to 10000", - "kinesis-request-timeout": "Request timeout in seconds", - "kinesis-request-timeout-required": "Request timeout is required", - "other-properties": "Other properties", - "subscription-tags": "Subscription tags", - "remove-subscription-tag": "Remove subscription tag", - "add-subscription-tag": "Add subscription tag", - "add-subscription-tag-prompt": "Please add subscription tag", - "key": "Key", - "path": "Path", - "required": "Required", - "integration-key": "Integration key", - "copy-integration-key": "Copy integration key", - "integration-key-copied-message": "Integration key has been copied to clipboard", - "integration-secret": "Integration secret", - "copy-integration-secret": "Copy integration secret", - "integration-secret-copied-message": "Integration secret has been copied to clipboard", - "execute-remotely": "Execute remotely", - "remote": "Remote", - "handler-configuration": "Handler Configuration", - "handler-configuration-type": "Handler Type", - "so-broadcast": "Enable broadcast - integration will accepts broadcast address packets", - "so-keepalive-option": "Enable sending of keep-alive messages on connection-oriented sockets", - "so-reuse-addr": "Bind the process to a port", - "tcp-no-delay": "Forces a socket to send the data without buffering (disable Nagle's buffering algorithm)", - "fail-fast": "Thrown exception soon as the decoder notices the length of the frame will exceed max size", - "strip-delimiter": "Strip Delimiter", - "length-field-offset": "Length Field Offset", - "length-field-offset-required": "Length Field Offset is required.", - "length-field-offset-range": "Length Field Offset should be in a range from 0 to 8.", - "length-field-length": "Length Field Length", - "length-field-length-required": "Length Field Length is required.", - "length-field-length-range": "Length Field Length should be in a range from 0 to 8.", - "length-adjustment": "Length Adjustment (the compensation value to add to the value of the length field)", - "length-adjustment-required": "Length Adjustment is required.", - "length-adjustment-range": "Length Adjustment should be in a range from 0 to 8.", - "byte-order": "Byte Order of the length field", - "initial-bytes-to-strip": "Number of first bytes to strip out from the decoded frame", - "initial-bytes-to-strip-required": "Number of first bytes to strip out from the decoded frame is required.", - "initial-bytes-to-strip-range": "Number of first bytes to strip out from the decoded frame should be in a range from 0 to 8.", - "so-backlog-option": "Max number of pending connects on the socket", - "so-backlog-option-required": "The max number of pending connects on the socket is required.", - "so-backlog-option-range": "The max number of pending connects on the socket should be in a range from 1 to 65535.", - "so-rcv-buf": "Size of the buffer for inbound socket (in KB)", - "so-rcv-buf-required": "Size of the buffer for inbound socket (in KB) is required.", - "so-rcv-buf-range": "Size of the buffer for inbound socket (in KB) should be in a range from 1 to 65535.", - "so-snd-buf": "Size of the buffer for outbound socket (in KB)", - "so-snd-buf-required": "Size of the buffer for outbound socket (in KB) is required.", - "so-snd-buf-range": "Size of the buffer for outbound socket (in KB) should be in a range from 1 to 65535.", - "charset-name": "Charset Name", - "charset-name-required": "Charset Name is required.", - "message-separator": "Message Separator", - "message-separator-required": "Message Separator is required.", - "character-sequence": "Character Sequence", - "character-sequence-required": "Character Sequence is required.", - "max-frame-length": "Max Frame Length (in bytes)", - "max-frame-length-required": "Max Frame Length (in bytes) is required.", - "max-frame-length-range": "Max Frame Length (in bytes) should be in a range from 1 to 65535.", - "handler-type": "Handler Type", - "message-size": "Message Size", - "message-size-required": "Message Size is required.", - "service-url": "Service URL", - "service-url-required": "Service URL is required.", - "subscription-name": "Subscription name", - "subscription-name-required": "Subscription name is required.", - "max-num-messages": "Max number of messages", - "max-num-messages-required": "Max number of messages is required.", - "max-num-bytes": "Max number of bytes", - "max-num-bytes-required": "Max number of bytes required.", - "timeout-in-ms": "Timeout in milliseconds", - "timeout-in-ms-required": "Timeout in milliseconds is required.", - "user-id": "User ID", - "user-id-required": "User ID is required.", - "token": "Token", - "token-required": "Token is required.", - "project-id": "Project ID", - "project-id-required": "Project ID is required.", - "subscription-id": "Subscription ID", - "subscription-id-required": "Subscription ID is required.", - "service-account-key": "Service Account key file", - "service-account-key-required": "Service Account key file is required.", - "tcp": { - "system-line-separator": "System Line Separator", - "nul-delimiter": "Nul Delimiter", - "byte-order-little-endian": "Little Endian", - "byte-order-big-endian": "Big Endian" - }, - "cache-size": "Cache Size", - "cache-time-to-live": "Cache time to live in minutes", - "min-cache-size": "Cache size can't be lower 0", - "min-cache-time-to-live": "Cache time to live can't be lower 0", - "max-cache-time-to-live": "Invalid time of cache live, select between 0 and 525600", - "coap-security-mode": "Security mode", - "coap-security-mode-required": "CoAP security mode is required", - "coap-security-mode-no-secure": "NO SECURE", - "coap-security-mode-dtls": "DTLS", - "coap-security-mode-mixed": "MIXED", - "coap-endpoint": "CoAP endpoint URL", - "coap-endpoint-url-copied-message": "CoAP endpoint URL has been copied to clipboard", - "copy-coap-endpoint-url": "Copy CoAP endpoint URL", - "copy-coap-dtls-endpoint-url": "Copy CoAP DTLS endpoint URL", - "coap-dtls-base-url": "DTLS Base URL", - "coap-dtls-base-url-required": "DTLS Base URL is required", - "coap-dtls-endpoint": "CoAP DTLS endpoint URL", - "coap-dtls-endpoint-url-copied-message": "CoAP DTLS endpoint URL has been copied to clipboard", - "unassign-integration-title": "Are you sure you want to unassign the integration '{{integrationName}}'?", - "unassign-integration-from-edge-text": "After the confirmation the integration will be unassigned and won't be accessible by the edge.", - "unassign-integrations-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 integration} other {# integrations} }?", - "unassign-integrations-from-edge-text": "After the confirmation all selected integrations will be unassigned and won't be accessible by the edge.", - "unassign-integrations": "Unassign integrations", - "edge-placeholder-hint": "You can use placeholder ${{ATTRIBUTE_KEY}} to substitute integration field with attribute value from specific Edge entity. For example, 'Edge A' has attribute 'baseUrl' that equals to 'http://localhost:9999'. You can set ${{baseUrl}} as one of the integration fields, and it is going to be replaced to 'http://localhost:9999' during the assignment of this integration to 'Edge A'. Additionally, if 'Edge A' attribute 'baseUrl' is going to be updated - integration with new updated value is going to be automatically provisioned to 'Edge A'.", - "status": { - "status": "Status", - "active": "Active", - "disabled": "Disabled", - "failed": "Failed", - "pending": "Pending" - }, - "daily-activity": "Daily activity", - "enable-debug-mode": "Enable debug mode", - "disable-debug-mode": "Disable debug mode", - "advanced-settings": "Advanced settings", - "basic-settings": "Basic settings", - "existing-converter": "Select existing", - "new-converter": "Create new", - "connection": "Connection", - "connected": "Connected", - "not-connected": "Not connected", - "environment": "Environment", - "environment-required": "Environment is required.", - "region-cn": "China", - "region-us": "United States", - "region-eu": "Europe", - "region-in": "India", - "topic-name": "Topic name", - "topic-name-required": "Topic name is required", - "sub-name": "Subscription name", - "sub-name-required": "Subscription name is required", - "downlink-connection-string": "Downlink connection string", - "downlink-connection-string-required": "Downlink connection string is required", - "downlink-topic-name": "Downlink topic name", - "downlink-topic-name-required": "Downlink topic name is required" - }, - "item": { - "selected": "Pasirinkta" - }, - "js-func": { - "no-return-error": "Funkcija privalo grąžinti reikšmę!", - "return-type-mismatch": "Funkcijos grąžinama reikšmė privalo būti '{{type}}' tipo!", - "tidy": "Tidy", - "mini": "Mini" + "single-switch": { + "behavior": "Elgsena", + "layout": "Išdėstymas", + "layout-right": "Dešinėje", + "layout-left": "Kairėje", + "layout-centered": "Centruota", + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "switch-color": "Jungiklio spalva", + "on": "Įjungta", + "off": "Išjungta", + "disabled": "Išjungta būsena", + "tumbler-color": "Perjungiklio spalva", + "on-label": "Įjungta etiketė", + "off-label": "Išjungta etiketė", + "switch": "Jungiklis" }, - "key-val": { - "key": "Raktas", - "value": "Reikšmė", - "remove-entry": "Panaiknti įrašą", - "add-entry": "Pridėti įrašą", - "no-data": "Įrašų nėra" - }, - "layout": { - "layout": "Maketas", - "layouts": "Maketai", - "manage": "Valdyti maketus", - "settings": "Maketų nustatymai", - "color": "Spalva", - "main": "Pagrindinis", - "right": "Į dešinę", - "left": "Į kairę", - "select": "Pasirinkti maketą", - "percentage-width": "Procentinis plotis (%)", - "fixed-width": "Fiksuotas plotis (px)", - "left-width": "Kairysis stulpelis (%)", - "right-width": "Dešinysis stulpelis (%)", - "pick-fixed-side": "Fiksuota pusė: ", - "layout-fixed-width": "Fiksuotas plotis (px)", - "value-min-error": "Reikšmė turi būti dedesnė nei {{min}}{{unit}}", - "value-max-error": "Reikšmė turi būti mažesnė nei {{max}}{{unit}}", - "layout-fixed-width-required": "Fiksuotas plotis būtinas", - "right-width-percentage-required": "Dešinės pusės procentas būtinas", - "left-width-percentage-required": "kairės pusės procentas būtinas", - "divider": "Daliklis", - "right-side": "Dešinės pusės maketas", - "left-side": "Kairės pusės maketas" - }, - "legend": { - "direction": "Elementų išdėstymo kryptis", - "position": "Legendos pozicija", - "show-values": "Rodyti reikšmes", - "min-option": "Min", - "max-option": "Max", - "average-option": "Vidurkis", - "total-option": "Viso", - "latest-option": "Naujausi", - "sort-legend": "Legendoje rodyti duomenų raktus", - "show-max": "Rodyti didžiausią reišmę", - "show-min": "Rodyti mažiausią reikšmę", - "show-avg": "Rodyti vidutinę reikšmę", - "show-total": "Rodyti suminę rekšmę", - "show-latest": "Rodyti naujausią reikšmę", - "settings": "Legendos nustatymai", - "min": "Min", - "max": "Max", - "avg": "Vidurkis", - "total": "Viso", - "latest": "Naujausia", - "comparison-time-ago": { - "previousInterval": "(previous interval)", - "customInterval": "(custom interval)", - "days": "(day ago)", - "weeks": "(week ago)", - "months": "(month ago)", - "years": "(year ago)" - } + "slider": { + "behavior": "Elgsena", + "initial-value": "Pradinė reikšmė", + "initial-value-hint": "Veiksmas, skirtas gauti pradinę slankiklio reikšmę.", + "on-value-change": "Keičiantis reikšmei", + "on-value-change-hint": "Veiksmas, paleidžiamas, kai pasikeičia slankiklio reikšmė.", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-extended": "Išplėstinis", + "layout-simplified": "Supaprastintas", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "range": "Intervalas", + "min": "min", + "max": "maks", + "range-ticks": "Intervalo žymės", + "tick-marks": "Padalos", + "colors": "Spalvos", + "main": "Pagrindinė", + "background": "Fonas", + "left-icon": "Kairioji piktograma", + "right-icon": "Dešinioji piktograma", + "slider": "Slankiklis" }, - "login": { - "login": "Prisijungti", - "request-password-reset": "Prašyti atstatyti slaptažodį", - "reset-password": "Atstatyti slaptažodį", - "create-password": "Nustatyti slaptažodį", - "two-factor-authentication": "Dviejų veiksnių autentifikavimas", - "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", - "password-again": "Slaptažodį įveskite dar kartą", - "sign-in": "Prisijunkite", - "username": "Vartoto vardas (el. paštas)", - "remember-me": "Prisiminti mane", - "forgot-password": "Pamiršote slaptažodį?", - "password-reset": "Slaptažodis atstatytas", - "expired-password-reset-message": "Jūsų slaptažodio galiojimas baigėsi! Nustatykite naują slaptažodį.", - "new-password": "Naujas slaptažodis", - "new-password-again": "Pakartokite naują slaptažodį", - "password-link-sent-message": "Atstatymo nuoroda išsiųsta", - "email": "El. paštas", - "no-account": "Neturi paskyros?", - "create-account": "Sukurti paskyrą", - "login-with": "Prisijungti kaip {{name}}", - "or": "arba", - "error": "Login error", - "verify-your-identity": "Verify your identity", - "select-way-to-verify": "Select a way to verify", - "resend-code": "Resend code", - "resend-code-wait": "Resend code in { time, plural, =1 {1 second} other {# seconds} }", - "try-another-way": "Try another way", - "totp-auth-description": "Please enter the security code from your authenticator app.", - "totp-auth-placeholder": "Code", - "sms-auth-description": "A security code has been sent to your phone at {{contact}}.", - "sms-auth-placeholder": "SMS code", - "email-auth-description": "A security code has been sent to your email address at {{contact}}.", - "email-auth-placeholder": "Email code", - "backup-code-auth-description": "Please enter one of your backup codes.", - "backup-code-auth-placeholder": "Backup code" - }, - "signup": { - "firstname": "Vardas", - "lastname": "Pavardė", - "email": "El. paštas", - "signup": "Registracija", - "create-password": "nustatyti slaptažodį", - "repeat-password": "Pakartokite slaptažodį", - "have-account": "Jau turite paskyrą?", - "signin": "Prisijungti", - "no-captcha-message": "Turite patvirtinti, jog nesate robotas", - "password-length-message": "Jūsų slaptažodis turi būti ne nei 6 simboliai", - "email-verification": "El. pašto adreso patvirtinimas", - "email-verification-message": "El. laiškas su patvirtinimo informacija išsiųstas nurodytu el. pašto adresu.
    Sekite laiške pateiktas intrukcijas ir atlikite prisijungimui reikalingus veiksmus.
    Pastaba: Jei nematote el. laiško, patikrinkite 'Šlamšto' katalogą arba bandykite dar kartą siųsti laišką paspausdami 'Siųsti dar kartą' mygtuką", - "account-activation-title": "Paskyros aktyvavimas", - "account-activated": "Paskyra aktyvuota!", - "account-activated-text": "Sveikiname!
    Jūsų paskyra aktyvuota.", - "resend": "Siųsti dar kartą", - "inactive-user-exists-title": "Neaktyvuotas vartotojas sistemoje jau yra", - "inactive-user-exists-text": "Vartotojas su tokiu el. pašto adresu jau užregistruotas, tačiau šis adresas nepatvirtintas.
    Paspauskite 'Siųsti dar kartą' mygtuką jei norite išsiųsti patvirtinimo laišką.", - "activating-account": "Paskyra aktyvuojama...", - "activating-account-text": "Jūsų paskyra aktyvuojama. Palaukite...", - "accept-privacy-policy": "Sutikti su privatumo politika", - "accept": "Sutikti", - "privacy-policy": "Privatumo politika", - "accept-privacy-policy-message": "You must accept our Privacy Policy", - "recaptcha-title": "reCAPTCHA", - "terms-of-use": "Terms of Use", - "accept-terms-of-use-message": "You must accept our Terms Of Use" + "value-card": { + "layout": "Išdėstymas", + "layout-square": "Kvadratinis", + "layout-vertical": "Vertikalus", + "layout-centered": "Centruotas", + "layout-simplified": "Supaprastintas", + "layout-horizontal": "Horizontalus", + "layout-horizontal-reversed": "Apverstas horizontalus", + "label": "Etiketė", + "icon": "Piktograma", + "value": "Reikšmė", + "date": "Data", + "value-card-style": "Reikšmės kortelės stilius", + "auto-scale": "Automatinis mastelis" + }, + "label-card": { + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "label-card-style": "Etiketės kortelės stilius" + }, + "label-value-card": { + "value": "Reikšmė", + "label-value-card-style": "Etiketės ir reikšmės kortelės stilius" + }, + "liquid-level-card": { + "layout-simple": "Paprastas", + "layout-percentage": "Procentinis", + "layout-absolute": "Absoliutus", + "layout": "Išdėstymas", + "background-overlay": "Reikšmės fono perdanga", + "total-volume": "Bendras tūris", + "total-volume-units": "Bendro tūrio vienetai", + "tank": "Talpa", + "shape": "Forma", + "datasource-units": "Šaltinio vienetai", + "widget-units": "Valdiklio vienetai", + "decimals": "Dešimtainės", + "liquid": "Skystis", + "liquid-color": "Skysčio spalva", + "value": "Reikšmė", + "value-font": "Reikšmės šriftas", + "level": "Lygis", + "last-update": "Paskutinis atnaujinimas", + "shape-by-attribute": "Nustatyti talpos formą pagal atributo pavadinimą", + "tooltip-background": "Fono spalva", + "background-blur": "Fono suliejimas", + "tank-color": "Talpos spalva", + "static": "Statinis", + "see-examples": "Peržiūrėti pavyzdžius", + "attribute": "Atributas", + "shape-type": "Tipas", + "v-oval": "Vertikali ovalo forma", + "v-cylinder": "Vertikalus cilindras", + "v-capsule": "Vertikali kapsulė", + "rectangle": "Stačiakampis", + "h-oval": "Horizontalus ovalas", + "h-ellipse": "Horizontali elipsė", + "h-dish-ends": "Horizontalūs išgaubti galai", + "h-cylinder": "Horizontalus cilindras", + "h-capsule": "Horizontali kapsulė", + "h-elliptical_2_1": "Horizontalus 2:1 elipsinis", + "icon": "Kortelės piktograma", + "title": "Kortelės pavadinimas", + "units": "Vienetai", + "color-and-font": "Spalva ir šriftas", + "shape-attribute-name": "Atributo pavadinimas", + "total-volume-required": "Bendras tūris yra privalomas.", + "attribute-name-required": "Atributo pavadinimas yra privalomas.", + "attribute-key-not-set": "Atributo '{{attributeName}}' raktas nenustatytas", + "attribute-key-invalid": "Atributo '{{attributeName}}' raktas neteisingas" + }, + "aggregated-value-card": { + "subtitle": "Paantraštė", + "chart": "Diagrama", + "values": "Reikšmės", + "value-appearance": "Reikšmės išvaizda", + "position": "Padėtis", + "position-center": "Centre", + "position-right-top": "Dešinėje viršuje", + "position-right-bottom": "Dešinėje apačioje", + "position-left-top": "Kairėje viršuje", + "position-left-bottom": "Kairėje apačioje", + "font": "Šriftas", + "color": "Spalva", + "arrow": "Rodyklė", + "display-up-down-arrow": "Rodyti aukštyn/žemyn rodyklę", + "add-value": "Pridėti reikšmę", + "remove-value": "Pašalinti reikšmę", + "no-values": "Reikšmės nesukonfigūruotos", + "aggregation": "Agregavimas", + "aggregated-value-card-style": "Agreguotų reikšmių kortelės stilius", + "auto-scale": "Automatinis mastelis" + }, + "value-chart-card": { + "layout": "Išdėstymas", + "layout-left": "Kairėje", + "layout-right": "Dešinėje", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "chart": "Diagrama", + "value-chart-card-style": "Reikšmės ir diagramos kortelės stilius" + }, + "progress-bar": { + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-simplified": "Supaprastintas", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "range": "Intervalas", + "min": "min", + "max": "maks", + "range-ticks": "Intervalo žymės", + "bar": "Juosta", + "bar-color": "Juostos spalva", + "bar-background": "Juostos fonas", + "progress-bar-card-style": "Progreso juostos kortelės stilius" }, "notification": { - "action-button": "Action button", - "action-type": "Action type", - "active": "Active", - "add-notification-recipients-group": "Add notification recipients group", - "add-notification-template": "Add notification template", - "add-recipient": "Add recipient", - "add-recipients": "Add recipients", - "add-rule": "Add rule", - "add-stage": "Add stage", - "add-template": "Add template", - "after": "After", - "alarm-assignment-trigger-settings": "Alarm assignment trigger settings", - "alarm-comment-trigger-settings": "Alarm comment trigger settings", - "alarm-trigger-settings": "Alarm trigger settings", - "all": "All", - "api-feature-hint": "If the field is empty, the trigger will be applied to all api features", - "api-usage-trigger-settings": "API usage trigger settings", - "new-platform-version-trigger-settings": "New platform version trigger settings", - "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", - "at-least-one-should-be-selected": "At least one should be selected", - "basic-settings": "Basic settings", - "button-text": "Button text", - "button-text-required": "Button text is required", - "button-text-max-length": "Button text should be less than or equal to {{ length }} characters", - "compose": "Compose", - "conversation": "Conversation", - "conversation-required": "Conversation is required", - "copy-notification-template": "Copy notification template", - "copy-rule": "Copy rule", - "copy-template": "Copy template", - "create-new": "Create new", - "created": "Created", - "delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.", - "delete-notification-title": "Are you sure you want to delete the notification?", - "delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.", - "delete-notifications-title": "Are you sure you want to delete { count, plural, =1 {1 notification} other {# notifications} }?", - "delete-recipient-text": "Be careful, after the confirmation the recipient will become unrecoverable.", - "delete-recipient-title": "Are you sure you want to delete recipient '{{recipientName}}'?", - "delete-recipients-text": "Be careful, after the confirmation recipients will become unrecoverable.", - "delete-recipients-title": "Are you sure you want to delete { count, plural, =1 {1 recipient} other {# recipients} }?", - "delete-request-text": "Be careful, after the confirmation request will become unrecoverable.", - "delete-request-title": "Are you sure you want to delete request?", - "delete-requests-text": "Be careful, after the confirmation requests will become unrecoverable.", - "delete-requests-title": "Are you sure you want to delete { count, plural, =1 {1 request} other {# requests} }?", - "delete-rule-text": "Be careful, after the confirmation rule will become unrecoverable.", - "delete-rule-title": "Are you sure you want to delete rule '{{ruleName}}'?", - "delete-rules-text": "Be careful, after the confirmation rules will become unrecoverable.", - "delete-rules-title": "Are you sure you want to delete { count, plural, =1 {1 rule} other {# rules} }?", - "delete-template-text": "Be careful, after the confirmation template will become unrecoverable.", - "delete-template-title": "Are you sure you want to delete template '{{templateName}}'?", - "delete-templates-text": "Be careful, after the confirmation templates will become unrecoverable.", - "delete-templates-title": "Are you sure you want to delete { count, plural, =1 {1 template} other {# templates} }?", - "deleted": "Deleted", - "delivery-method": { - "delivery-method": "Delivery method", - "email": "Email", - "email-preview": "Email notification preview", - "slack": "Slack", - "slack-preview": "Slack notification preview", - "microsoft-teams": "Microsoft Teams", - "microsoft-teams-preview": "Microsoft Teams notification preview", - "sms": "SMS", - "sms-preview": "SMS notification preview", - "web": "Web", - "web-preview": "Web notification preview" - }, - "delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.", - "delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.", - "delivery-methods": "Delivery methods", - "description": "Description", - "device-activity-trigger-settings": "Device active trigger settings", - "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", - "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", - "disabled": "Disabled", - "edit-notification-recipients-group": "Edit notification recipients group", - "edit-notification-template": "Edit notification template", - "edit-rule": "Edit rule", - "edit-template": "Edit template", - "enabled": "Enabled", - "entities-limit-trigger-settings": "Entities limit trigger settings", - "entity-action-trigger-settings": "Entity action trigger settings", - "entity-type": "Entity type", - "escalation-chain": "Escalation chain", - "failed-send": "Sending failures", - "fails": "{ count, plural, =1 {1 fail} other {# fails} }", - "filter": "Filter", - "first-recipient": "First recipient", - "inactive": "Inactive", - "inbox": "Inbox", - "notification-inbox": "Notifications / Inbox", - "input-field-support-templatization": "Input field support templatization.", - "input-fields-support-templatization": "Input fields support templatization.", - "integration-action-trigger-settings": "Integration events trigger settings", - "integration-list-rule-hint": "If the field is empty, the trigger will be applied to all integrations", - "link": "Link", - "link-required": "Link is required", - "link-type": { - "dashboard": "Open dashboard", - "link": "Open URL link" - }, - "loading-notifications": "Loading notifications...", - "management": "Notification management", - "mark-all-as-read": "Mark all as read", - "mark-as-read": "Mark as read", - "message": "Message", - "message-required": "Message is required", - "message-max-length": "Message should be less than or equal to {{ length }} characters", - "name": "Name", - "name-required": "Name is required", - "new-notification": "New notification", - "no-inbox-notification": "No notification found", - "no-notification-request": "No notification request", - "no-notification-templates": "No notification templates found", - "no-notifications-yet": "No notifications yet", - "no-recipients-notification": "No recipients notification", - "no-rule": "No rule configured", - "no-rules-notification": "No rules notification", - "no-severity-found": "No severity found", - "no-severity-matching": "'{{severity}}' not found.", - "no-template-matching": "No resource matching '{{template}}' were found.", - "not-found-slack-recipient": "Slack recipient not found", - "notification": "Notification", - "notification-center": "Notification center", - "notify": "notify", - "notify-again": "Notify again", - "notify-alarm-action": { - "acknowledged": "Alarm acknowledged", - "assigned": "Alarm assigned", - "cleared": "Alarm cleared", - "created": "Alarm created", - "severity-changed": "Alarm severity changed", - "unassigned": "Alarm unassigned" - }, - "notify-on": "Notify on", - "notify-on-comment-update": "Notify on comment update", - "notify-on-required": "Notify on is required", - "notify-on-unassign": "Notify on unassign", - "notify-only-integrations-errors": "Only notify on error", - "notify-only-user-comments": "Notify only user comments", - "only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures", - "only-rule-node-lifecycle-failures": "Only rule node lifecycle failures", - "platform-users": "Platform users", - "rate-limits": "Rate limits", - "rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits", - "recipient": "Recipient", - "recipient-group": "Recipient group", - "recipient-type": { - "affected-tenant-administrators": "Affected tenant administrators", - "affected-user": "Affected user", - "affected-user-hint": "Affected user hint", - "all-users": "All users", - "customer-users": "Customer users", - "system-administrators": "System administrators", - "tenant-administrators": "Tenant administrators", - "user-filters": "User filter", - "user-group-list": "User group list", - "user-list": "User list", - "user-role": "User role", - "users-entity-owner": "Users of the entity owner", - "users-entity-owner-hint": "Users of the entity owner hint" - }, - "recipients": "Recipients", - "notification-recipients": "Notifications / Recipients", - "recipients-count": "{ count, plural, =1 {1 recipient} other {# recipients} }", - "recipients-required": "Recipients are required", - "refresh-allow-delivery-method": "Refresh allow delivery method", - "request-search": "Request search", - "request-status": { - "processing": "Processing", - "scheduled": "Scheduled", - "sent": "Sent" - }, - "review": "Review", - "rule": "Rule", - "rule-chain-list-rule-hint": "If the field is empty, the trigger will be applied to all rule chains", - "rule-engine-events-trigger-settings": "Rule engine events trigger settings", - "rule-engine-filter": "Rule engine filter", - "rule-name": "Rule name", - "rule-name-required": "Name is required", - "rule-disable": "Disable notification rule", - "rule-enable": "Enable notification rule", - "rule-node-filter": "Rule node filter", - "rules": "Rules", - "notification-rules": "Notifications / Rules", - "scheduler-later": "Schedule for later", - "search-notification": "Search notifications", - "search-recipients": "Search recipients", - "search-rules": "Search rules", - "search-templates": "Search templates", - "see-documentation": "See documentation", - "selected-notifications": "{ count, plural, =1 {1 notification} other {# notifications} } selected", - "selected-recipients": "{ count, plural, =1 {1 recipient} other {# recipients} } selected", - "selected-requests": "{ count, plural, =1 {1 request} other {# requests} } selected", - "selected-rules": "{ count, plural, =1 {1 rule} other {# rules} } selected", - "selected-template": "{ count, plural, =1 {1 template} other {# templates} } selected", - "send-notification": "Send notification", - "sent": "Sent", - "notification-sent": "Notifications / Sent", - "set-entity-from-notification": "Set entity from notification to dashboard state", - "slack-chanel-type": "Slack channel type", - "slack-chanel-types": { - "direct": "Direct message", - "private-channel": "Private channel", - "public-channel": "Public channel" - }, - "start-from-scratch": "Start from scratch", - "status": "Status", - "stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:", - "subject": "Subject", - "subject-required": "Subject is required", - "template": "Template", - "template-name": "Template name", - "template-required": "Template is required", - "template-type": { - "alarm": "Alarm", - "alarm-assignment": "Alarm assignment", - "alarm-comment": "Alarm comment", - "api-usage-limit": "API usage limit", - "device-activity": "Device activity", - "entities-limit": "Entities limit", - "entity-action": "Entity action", - "general": "General", - "integration-lifecycle-event": "Integration lifecycle event", - "rule-engine-lifecycle-event": "Rule engine lifecycle event", - "rule-node": "Rule node", - "new-platform-version": "New platform version", - "rate-limits": "Exceeded rate limits" - }, - "templates": "Templates", - "notification-templates": "Notifications / Templates", - "tenant-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all tenant profiles", - "tenants-list-rule-hint": "If the field is empty, the trigger will be applied to all tenants", - "threshold": "Threshold", - "theme-color": "Theme color", - "time": "Time", - "track-rule-node-events": "Track rule node events", - "trigger": { - "alarm": "Alarm", - "alarm-assignment": "Alarm assignment", - "alarm-comment": "Alarm comment", - "api-usage-limit": "API usage limit", - "device-activity": "Device activity", - "entities-limit": "Entities limit", - "entity-action": "Entity action", - "integration-lifecycle-event": "Integration lifecycle event", - "rule-engine-lifecycle-event": "Rule engine lifecycle event", - "new-platform-version": "New platform version", - "rate-limits": "Exceeded rate limits", - "trigger": "Trigger", - "trigger-required": "Trigger is required" - }, - "type": "Type", - "unread": "Unread", - "updated": "Updated", - "use-template": "Use template", - "view-all": "View all", - "view-notification-recipients-group": "View notification recipients group", - "view-notification-template": "View notification template", - "view-rule": "View rule", - "warning": "Warning", - "webhook-url": "Webhook URL", - "webhook-url-required": "Webhook URL is required", - "channel-name": "Channel name", - "channel-name-required": "Channel name is required", - "settings": { - "notification-settings": "Notification settings", - "reset-all": "Reset all settings", - "reset-all-title": "Are you sure you want to reset form?", - "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", - "type": "Type", - "enable-all": "Enable all", - "disable-all": "Disable all", - "delivery-not-configured": "Delivery method is not configured" - } + "max-notification-display": "Maksimalus rodomų pranešimų skaičius", + "counter": "Skaitiklis", + "counter-hint": "Skaitiklis bus rodomas, jei įjungtas „Valdiklio pavadinimas“", + "icon": "Piktograma", + "counter-value": "Reikšmė", + "counter-color": "Spalva", + "notification-button": "Pranešimų mygtukai", + "button-view-all": "Peržiūrėti visus", + "button-filter": "Filtruoti", + "type-filter": "Tipų filtras", + "button-mark-read": "Pažymėti visus kaip skaitytus", + "notification-types": "Pranešimų tipai", + "notification-type": "Pranešimo tipas", + "search-type": "Paieškos tipas", + "any-type": "Bet koks tipas" }, - "ota-update": { - "add": "Add package", - "assign-firmware": "Assigned firmware", - "assign-firmware-required": "Assigned firmware is required", - "assign-software": "Assigned software", - "assign-software-required": "Assigned software is required", - "auto-generate-checksum": "Auto-generate checksum", - "cant-applied-group-all": "Can't be applied for group All", - "checksum": "Checksum", - "checksum-hint": "If checksum is empty, it will be generated automatically", - "checksum-algorithm": "Checksum algorithm", - "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} }.", - "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", - "content-type": "Content type", - "copy-checksum": "Copy checksum", - "copy-direct-url": "Copy direct URL", - "copyId": "Copy package Id", - "copied": "Copied!", - "delete": "Delete package", - "delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.", - "delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?", - "delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.", - "delete-ota-updates-title": "Are you sure you want to delete { count, plural, =1 {1 OTA update} other {# OTA updates} }?", - "description": "Description", - "direct-url": "Direct URL", - "direct-url-copied-message": "Package direct URL has been copied to clipboard", - "direct-url-required": "Direct URL is required", - "download": "Download package", - "drop-file": "Drop a package file or click to select a file to upload.", - "drop-package-file-or": "Drag and drop a package file or", - "file-name": "File name", - "file-size": "File size", - "file-size-bytes": "File size in bytes", - "idCopiedMessage": "Package Id has been copied to clipboard", - "no-firmware-matching": "No compatible Firmware OTA Update packages matching '{{entity}}' were found.", - "no-firmware-text": "No compatible Firmware OTA Update packages provisioned.", - "no-packages-text": "No packages found", - "no-software-matching": "No compatible Software OTA Update packages matching '{{entity}}' were found.", - "no-software-text": "No compatible Software OTA Update packages provisioned.", - "ota-update": "OTA update", - "ota-update-details": "OTA update details", - "ota-updates": "OTA updates", - "package-type": "Package type", - "packages-repository": "Packages repository", - "search": "Search packages", - "selected-package": "{ count, plural, =1 {1 package} other {# packages} } selected", - "title": "Title", - "title-required": "Title is required.", - "title-max-length": "Title should be less than 256", - "types": { - "firmware": "Firmware", - "software": "Software" - }, - "upload-binary-file": "Upload binary file", - "use-external-url": "Use external URL", - "version": "Version", - "version-required": "Version is required.", - "version-tag": "Version Tag", - "version-tag-hint": "Custom tag should match the package version reported by your device.", - "version-max-length": "Version should be less than 256", - "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type." - }, - "position": { - "top": "Viršus", - "bottom": "Apačia", - "left": "Kairioji pusė", - "right": "Dešinioji pusė" - }, - "profile": { - "profile": "Profilis", - "last-login-time": "Paskutinio prisijungimo laikas", - "change-password": "Pakeisti slaptažodį", - "current-password": "Dabartinis slaptažodis", - "copy-jwt-token": "Copy JWT token", - "jwt-token": "JWT token", - "token-valid-till": "Token is valid till", - "tokenCopiedSuccessMessage": "JWT token has been copied to clipboard", - "tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page." - }, - "profiles": { - "profiles": "Profiles" - }, - "security": { - "security": "Security", - "general-settings": "General security settings", - "access-token": "Access token", - "access-token-required": "Access token is required", - "clientId": "Client ID", - "clientId-required": "Client ID is required", - "username": "Username", - "username-required": "Username is required", - "ca-cert": "CA certificate", - "2fa": { - "2fa": "Two-factor authentication", - "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.", - "authenticate-with": "You can authenticate with:", - "disable-2fa-provider-text": "Disabling {{name}} will make your account less secure", - "disable-2fa-provider-title": "Are you sure you want to disable {{name}}?", - "get-new-code": "Get new code", - "main-2fa-method": "Use as main two-factor authentication method", - "dialog": { - "activation-step-description-email": "The next time you login in, you will be prompted to enter the security code that will be sent to your email address.", - "activation-step-description-sms": "The next time you login in, you will be prompted to enter the security code that will be sent to the phone number.", - "activation-step-description-totp": "The next time you login in, you will need to provide a two-factor authentication code.", - "activation-step-label": "Activation", - "backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.", - "backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.", - "download-txt": "Download (txt)", - "email-step-description": "Enter an email to use as your authenticator.", - "email-step-label": "Email", - "enable-email-title": "Enable email authenticator", - "enable-sms-title": "Enable SMS authenticator", - "enable-totp-title": "Enable authenticator app", - "enter-verification-code": "Enter the 6-digit code here", - "get-backup-code-title": "Get backup code", - "next": "Next", - "scan-qr-code": "Scan this QR code with your verification app", - "send-code": "Send code", - "sms-step-description": "Enter a phone number to use as your authenticator.", - "sms-step-label": "Phone Number", - "success": "Success!", - "totp-step-description-install": "You can install apps like Google Authenticator, Authy, or Duo.", - "totp-step-description-open": "Open the authenticator app on your mobile phone.", - "totp-step-label": "Get app", - "verification-code": "6-digit code", - "verification-code-invalid": "Invalid verification code format", - "verification-code-incorrect": "Verification code is incorrect", - "verification-code-many-request": "Too many requests check verification code", - "verification-step-description": "Enter a 6-digit code we just sent to {{address}}", - "verification-step-label": "Verification" - }, - "provider": { - "email": "Email", - "email-description": "Use a security code sent to your email address to authenticate.", - "email-hint": "Authentication codes are sent via email to {{ info }}", - "sms": "SMS", - "sms-description": "Use your phone to authenticate. We'll send you a security code via SMS message when you log in.", - "sms-hint": "Authentication codes are sent by text message to {{ info }}", - "totp": "Authenticator app", - "totp-description": "Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.", - "totp-hint": "Authenticator app is set up for your account", - "backup_code": "Backup code", - "backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when you’re traveling.", - "backup-code-hint": "{{ info }} single-use codes are active at this time" - } - }, - "password-requirement": { - "at-least": "At least:", - "character": "{ count, plural, =1 {1 character} other {# characters} }", - "digit": "{ count, plural, =1 {1 digit} other {# digits} }", - "incorrect-password-try-again": "Incorrect password. Try again", - "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", - "new-passwords-not-match": "New password didn't match", - "password-should-not-contain-spaces": "Your password should not contain spaces", - "password-not-meet-requirements": "Password didn't meet requirements", - "password-requirements": "Password requirements", - "password-should-difference": "New password should be different from current", - "special-character": "{ count, plural, =1 {1 special character} other {# special characters} }", - "uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }" - } + "alarm-count": { + "alarm-count-card-style": "Signalizacijų skaičiaus kortelės stilius" }, - "relation": { - "relations": "Ryšiai", - "direction": "Direction", - "clear-relation-type": "Clear relation type", - "search-direction": { - "FROM": "Iš", - "TO": "Į" - }, - "direction-type": { - "FROM": "iš", - "TO": "į" - }, - "from-relations": "Išeinantys ryšiai", - "to-relations": "įeinantys ryšiai", - "selected-relations": "Pasirinkta { count, plural, =1 {1 ryšys} other {# ryšiai} } selected", - "type": "Tipas", - "to-entity-type": "Į subjekto tipą", - "to-entity-name": "Į subjektą", - "from-entity-type": "iš subjektų tipo", - "from-entity-name": "Iš subjekto", - "to-entity": "į subjektą", - "from-entity": "Iš subjekto", - "delete": "panaikinti ryšį", - "relation-type": "Ryšio tipas", - "relation-type-required": "Ryšio tipas būtinas.", - "relation-type-max-length": "Ryšio tipas negali viršyti 256 simbolių", - "any-relation-type": "Bet kuris tipas", - "add": "pridėti ryšį", - "edit": "Redaguoti ryšį", - "view": "peržiūrėti ryšį", - "delete-to-relation-title": "Ar tikrai norite panaikinti ryšį su '{{entityName}}' subjektu?", - "delete-to-relation-text": "Būkite dėmesingi, po patvirtinimo '{{entityName}}' subjektas neteks ryšio su dabartiniu subjektu.", - "delete-to-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", - "delete-to-relations-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti ryšiai bus panaikinti ir atitinkami subjektai neteks ryšių su dabartiniu subjektu.", - "delete-from-relation-title": "Ar tikrai norite panaikinti ryšį iš '{{entityName}}' subjekto?", - "delete-from-relation-text": "Būkite dėmesingi, po patvirtinimo, dabartinis subjektas neteks ryšio su '{{entityName}}' subjektu.", - "delete-from-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", - "delete-from-relations-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti ryšiai bus panaikinti ir dabartinis subjektas neteks ryšių su atitinkamais subjektais.", - "remove-relation-filter": "Panaikinti ryšių filtrą", - "remove-filter": "Panakinti filtrą", - "add-relation-filter": "Pridėti ryšių filtrą", - "any-relation": "Bet kuris ryšys", - "relation-filters": "Ryšių filtrai", - "additional-info": "Papildoma informacija (JSON)", - "invalid-additional-info": "Papildomos JSON informacijos išanalizuoti nepavyko.", - "no-relations-text": "Ryšių nėra" - }, - "resource": { - "add": "Add Resource", - "all-types": "All", - "copyId": "Copy resource Id", - "delete": "Delete resource", - "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", - "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", - "delete-resources-action-title": "Delete { count, plural, =1 {1 resource} other {# resources} }", - "delete-resources-text": "Please note that the selected resources, even if they are used in device profiles, will be deleted.", - "delete-resources-title": "Are you sure you want to delete { count, plural, =1 {1 resource} other {# resources} }?", - "download": "Download resource", - "drop-file": "Drop a resource file or click to select a file to upload.", - "drop-resource-file-or": "Drag and drop a resource file or", - "empty": "Resource is empty", - "file-name": "File name", - "idCopiedMessage": "Resource Id has been copied to clipboard", - "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", - "no-resource-text": "No resources found", - "open-widgets-bundle": "Open widgets bundle", - "resource": "Resource", - "resource-library-details": "Resource details", - "resource-type": "Resource type", - "resources-library": "Resources library", - "search": "Search resources", - "selected-resources": "{ count, plural, =1 {1 resource} other {# resources} } selected", - "system": "System", - "title": "Title", - "title-required": "Title is required.", - "title-max-length": "Title should be less than 256", - "type": { - "jks": "JKS", - "js-module": "JS module", - "lwm2m-model": "LWM2M model", - "pkcs-12": "PKCS #12" - } + "entity-count": { + "entity-count-card-style": "Objektų skaičiaus kortelės stilius" }, - "rulechain": { - "rulechain": "Rule chain", - "rulechain-events": "Rule chain events", - "rulechains": "Rule chains", - "root": "Root", - "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "add": "Add Rule Chain", - "set-root": "Make rule chain root", - "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", - "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", - "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", - "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", - "delete-rulechains-title": "Are you sure you want to delete { count, plural, =1 {1 rule chain} other {# rule chains} }?", - "delete-rulechains-action-title": "Delete { count, plural, =1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", - "export-failed-error": "Unable to export rule chain: {{error}}", - "create-new-rulechain": "Create new rule chain", - "rulechain-file": "Rule chain file", - "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", - "copyId": "Copy rule chain Id", - "idCopiedMessage": "Rule chain Id has been copied to clipboard", - "select-rulechain": "Select rule chain", - "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", - "rulechain-required": "Rule chain is required", - "management": "Rules management", - "debug-mode": "Debug mode", - "search": "Search rule chains", - "selected-rulechains": "{ count, plural, =1 {1 rule chain} other {# rule chains} } selected", - "open-rulechain": "Open rule chain", - "edge-template-root": "Template Root", - "assign-to-edge": "Assign to Edge", - "edge-rulechain": "Edge rule chain", - "unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.", - "unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 rulechain} other {# rulechains} }?", - "unassign-rulechains-from-edge-text": "After the confirmation all selected rulechains will be unassigned and won't be accessible by the edge.", - "assign-rulechain-to-edge-title": "Assign Rule Chain(s) To Edge", - "assign-rulechain-to-edge-text": "Please select the rulechains to assign to the edge", - "set-edge-template-root-rulechain": "Make rule chain as edge template root", - "set-edge-tscheduler-eventemplate-root-rulechain": "Make rule chain as edge template root", - "set-edge-template-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' edge template root?", - "set-edge-template-root-rulechain-text": "After the confirmation the rule chain will become edge template root and will be root rule chain for a newly created edges.", - "invalid-rulechain-type-error": "Unable to import rule chain: Invalid rule chain type. Expected type is {{expectedRuleChainType}}.", - "set-auto-assign-to-edge": "Assign rule chain to edge(s) on creation", - "set-auto-assign-to-edge-title": "Are you sure you want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", - "set-auto-assign-to-edge-text": "After the confirmation the edge rule chain will be automatically assigned to edge(s) on creation.", - "unset-auto-assign-to-edge": "Do not assign rule chain to edge(s) on creation", - "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", - "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", - "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", - "unassign-rulechains": "Unassign rulechains" - }, - "rulenode": { - "rule-node-events": "Rule node events", - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "close-node-library": "Close node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "type": "Type", - "rule-node-description": "Rule node description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "create-nested-rulechain": "Create nested rule chain", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "link-labels": "Link labels", - "link-labels-required": "Link labels is required.", - "no-link-labels-found": "No link labels found", - "no-link-label-matching": "'{{label}}' not found.", - "create-new-link-label": "Create a new one!", - "type-filter": "Filter", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", - "type-enrichment-details": "Add additional information into Message Metadata", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", - "type-action-details": "Perform special action", - "type-analytics": "Analytics", - "type-analytics-details": "Perform analysis of streamed or persisted data", - "type-external": "External", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-flow": "Flow", - "type-flow-details": "Organizes message flow", - "type-input": "Input", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "type-unknown": "Unknown", - "type-unknown-details": "Unresolved Rule Node", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", - "script-lang-java-script": "Java Script", - "script-lang-tbel": "TBEL", - "message": "Message", - "message-type": "Message type", - "select-message-type": "Select message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help", - "test-with-this-message": "{{test}} with this message" - }, - "role": { - "role": "Rolė", - "roles": "Rolės", - "management": "Rolių valdymas", - "view-roles": "Peržiūrėti roles", - "no-roles-matching": "Rolių, atitinkančių '{{entity}}' nėra.", - "role-list": "Rolių sąrašas", - "role-list-required": "Rolių sąrašas būtinas", - "add": "Pridėti rolę", - "view": "Peržiūrėti rolę", - "search": "Rolių paieška", - "selected-roles": "Pasirinkta { count, plural, =1 {1 rolė} other {# rolės} }", - "no-roles-text": "Rolių nėra", - "role-details": "Informacija apie rolę", - "add-role-text": "Pridėti naują rolę", - "delete": "Panaikinti rolę", - "delete-roles": "Panaikinti roles", - "delete-role-title": "Ar tikrai norite panaikinti '{{roleName}}' rolę?", - "delete-role-text": "Būkite dėmesingi, po patvirtinimo, rolė ir visa su ja susijusi informacija bus pašalinta.", - "delete-roles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 rolę} other {# roles} }?", - "delete-roles-action-title": "Panaikinti { count, plural, =1 {1 rolę} other {# roles} }", - "delete-roles-text": "Būkite dėmesingi, po patvirtinimo, visos pasirinktos rolės ir su jomis susijusi informacija bus pašalintos.", - "role-type": "Tipas", - "role-type-required": "Rolės tipas būtinas.", - "select-role-type": "Pasirinkite rolės tipą", - "enter-role-type": "Įveskite rolės tipą", - "any-role": "Bet kuri rolė", - "no-role-types-matching": "Rolių tipų, atitinkančių '{{entitySubtype}}' nėra.", - "role-type-list-empty": "Nepasirinktas rolės tipas.", - "role-types": "Rolių tipais", - "created-time": "Sukųrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti rolės Id", - "idCopiedMessage": "Rolės Id nukopijuotas į iškarpinę", - "permissions": "Leidimai", - "role-required": "Rolė būtina", - "roles-required": "Rolės būtinos", - "display-type": { - "GENERIC": "Bendroji", - "GROUP": "Grupinė" - } + "count": { + "layout": "Išdėstymas", + "layout-column": "Stulpelis", + "layout-row": "Eilutė", + "label": "Etiketė", + "icon": "Piktograma", + "icon-background": "Piktogramos fonas", + "value": "Reikšmė", + "chevron": "Rodyklė", + "auto-scale": "Automatinis mastelis" }, - "group-permission": { - "user-group-roles": "Vartotojų grupės rolės", - "entity-group-permissions": "Subjektų grupės leidimai", - "role-type": "Rolės tipas", - "role-name": "Rolės pavadinimas", - "group-type": "Grupės tipas", - "group-name": "Grupės pavadinimas", - "group-owner": "Grupės savininkas", - "user-group-name": "Vartotojų grupės pavadinimas", - "user-group-owner": "Vartotojų grupės savininkas", - "edit": "Redaguoti leidimus", - "delete": "Naikinti leidimus", - "selected-group-permissions": "Pasirinkta { count, plural, =1 {1 grupinis leidimas} other {# grupiniai leidimai} }", - "delete-group-permission-title": "Ar tikrai norite panaikinti '{{roleName}}' grupinį leidimą?", - "delete-group-permission-text": "Būkite dėmesingi, po patvirtinimo, grupinis leidimas ir visa su juo sisijusi informacija bus pašalinta.", - "delete-group-permission": "Panaikinti grupinį leidima", - "delete-group-permissions-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 grupinį leidimą} other {# grupinius leidimus} }?", - "delete-group-permissions-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti grupiniai leidimai ir visa su jais susijusi informacija bus panaikinta ir atitinkami vartotojai neteks prieigos prie tam tikrų resursų.", - "delete-group-permissions": "Panaikinti grupinius leidimus", - "add-group-permission": "Pridėti grupinį leidimą", - "edit-group-permission": "Redaguoti grupinį leidimą", - "entity-group": "Subjektų grupė", - "user-group": "Vartotojų grupė", - "no-owners-matching": "Savininkų, atitinkančių '{{owner}}' nėra.", - "target-owner-required": "Subjektų grupės savininkas būtinas.", - "target-user-group-owner-required": "Vartotojų grupės savininkas būtinas.", - "no-group-permissions-text": "Grupinių leidimų nėra" - }, - "permission": { - "permissions-required": "Turi būti nurodytas nors vienas leidimas.", - "remove-permission": "Panaikinti leidimą", - "add-permission": "Pridėti leidimą", - "other": "Kita", - "resource": { - "resource": "Resursas", - "select-resource": "Pasirinkite resursą", - "resource-required": "Resursas būtinas", - "no-resources-matching": "Resursų, atitinkančių '{{resource}}' nėra.", - "display-type": { - "ALL": "Visi", - "PROFILE": "Profilis", - "ADMIN_SETTINGS": "Administratoriaus nustatymai", - "ALARM": "Įspėjimai", - "DEVICE": "Įrenginys", - "DEVICE_PROFILE": "Įrenginio profilis", - "ASSET": "Turtas", - "CUSTOMER": "Klientas", - "DASHBOARD": "Skydelis", - "ENTITY_VIEW": "Subjekto rodinys", - "EDGE": "Edge", - "TENANT": "Valdytojas", - "TENANT_PROFILE": "Valdytojo profilis", - "RULE_CHAIN": "Taisyklių grandinė", - "USER": "Vartotojas", - "WIDGETS_BUNDLE": "Valdiklių rinkinys", - "WIDGET_TYPE": "Valdiklio tipas", - "CONVERTER": "Keitiklis", - "INTEGRATION": "Integracija", - "SCHEDULER_EVENT": "Tvarkaraščio įvykis", - "BLOB_ENTITY": "Blob subjektas", - "CUSTOMER_GROUP": "Klientų grupė", - "DEVICE_GROUP": "Įrenginių grupė", - "ASSET_GROUP": "Turto grupė", - "USER_GROUP": "Vartotojų grupė", - "ENTITY_VIEW_GROUP": "Subjektų rodinių grupė", - "DASHBOARD_GROUP": "Skydelių grupė", - "ROLE": "Rolė", - "GROUP_PERMISSION": "Grupinis leidimas", - "WHITE_LABELING": "Tinkinimas", - "AUDIT_LOG": "Audito žurnalas", - "API_USAGE_STATE": "API Usage State", - "TB_RESOURCE": "Resource", - "EDGE_GROUP": "Edge Group", - "OTA_PACKAGE": "Ota package", - "QUEUE": "Queue", - "VERSION_CONTROL": "Version control", - "ASSET_PROFILE": "Asset Profile", - "NOTIFICATION": "Notification" - } - }, - "operation": { - "operation": "Operacija", - "operations": "Operacijos", - "operations-required": "Turi būti nurodyta nors viena operacija.", - "enter-operation": "Įveskite operaciją", - "no-operations-matching": "Operacijų, atitinkančių '{{operation}}' nėra.", - "display-type": { - "ALL": "Visi", - "CREATE": "Sukurti", - "READ": "Skaityti", - "WRITE": "Rašyti", - "DELETE": "Panaikinti", - "ASSIGN_TO_CUSTOMER": "Priskirti klientui", - "UNASSIGN_FROM_CUSTOMER": "Atsieti nuo kliento", - "RPC_CALL": "RPC kvietimas", - "READ_CREDENTIALS": "Skaityti įgaliojimus", - "WRITE_CREDENTIALS": "Rašyti įgaliojimus", - "READ_ATTRIBUTES": "Skaityti atributus", - "WRITE_ATTRIBUTES": "Rašyti atributus", - "READ_TELEMETRY": "Skaityti telemetriją", - "WRITE_TELEMETRY": "Rašyti telemetriją", - "CLAIM_DEVICES": "Patvirtinti įrenginius", - "IMPERSONATE": "Įeiti kaip vartotojui", - "CHANGE_OWNER": "Pakeisti savininką", - "ADD_TO_GROUP": "Įtraukti į grupę", - "REMOVE_FROM_GROUP": "Pašalinti iš grupės", - "SHARE_GROUP": "Bendrinti grupę", - "ASSIGN_TO_TENANT": "Priskirti valdytojui" - } - } + "table": { + "common-table-settings": "Bendrieji lentelės nustatymai", + "enable-search": "Įjungti paiešką", + "enable-sticky-header": "Visada rodyti antraštę", + "enable-sticky-action": "Visada rodyti veiksmų stulpelį", + "hidden-cell-button-display-mode": "Paslėpto mygtuko veiksmų rodymo režimas", + "show-empty-space-hidden-action": "Rodyti tuščią vietą vietoje paslėpto langelio mygtuko veiksmo", + "dont-reserve-space-hidden-action": "Nerezervuoti vietos paslėptiems veiksmų mygtukams", + "display-timestamp": "Rodyti laiko žymos stulpelį", + "display-pagination": "Rodyti puslapiavimą", + "default-page-size": "Numatytasis puslapio dydis", + "page-step-settings": "Puslapio žingsnių nustatymai", + "page-step-count": "Žingsnių skaičius", + "page-step-increment": "Žingsnio didinimas", + "page-step-count-format-message": "Turi būti sveikasis skaičius nuo 1 iki 100.", + "page-step-increment-format-message": "Turi būti sveikasis skaičius, ne mažesnis kaip 1.", + "use-entity-label-tab-name": "Naudoti objekto etiketę skirtuko pavadinime", + "hide-empty-lines": "Slėpti tuščias eilutes", + "row-style": "Eilutės stilius", + "use-row-style-function": "Naudoti eilutės stiliaus funkciją", + "row-style-function": "Eilutės stiliaus funkcija", + "cell-style": "Langelio stilius", + "use-cell-style-function": "Naudoti langelio stiliaus funkciją", + "cell-style-function": "Langelio stiliaus funkcija", + "cell-content": "Langelio turinys", + "use-cell-content-function": "Naudoti langelio turinio funkciją", + "cell-content-function": "Langelio turinio funkcija", + "show-latest-data-column": "Rodyti naujausių duomenų stulpelį", + "latest-data-column-order": "Naujausių duomenų stulpelio tvarka", + "entities-table-title": "Objektų lentelės pavadinimas", + "enable-select-column-display": "Įjungti pasirinktų stulpelių rodymą", + "display-entity-name": "Rodyti objekto pavadinimo stulpelį", + "entity-name-column-title": "Objekto pavadinimo stulpelio pavadinimas", + "display-entity-label": "Rodyti objekto etiketės stulpelį", + "entity-label-column-title": "Objekto etiketės stulpelio pavadinimas", + "display-entity-type": "Rodyti objekto tipo stulpelį", + "default-sort-order": "Numatytoji rūšiavimo tvarka", + "custom-title": "Tinkintas antraštės pavadinimas", + "column-width": "Stulpelio plotis (px arba %)", + "default-column-visibility": "Numatytasis stulpelio matomumas", + "column-visibility-visible": "Matomas", + "column-visibility-hidden": "Paslėptas", + "column-visibility-hidden-mobile": "Paslėptas mobiliuoju režimu", + "column-selection-to-display": "Stulpelių pasirinkimas rodymui", + "column-selection-to-display-enabled": "Įjungta", + "column-selection-to-display-disabled": "Išjungta", + "alarms-table-title": "Pavojaus signalų lentelės pavadinimas", + "enable-alarms-selection": "Įjungti signalų pasirinkimą", + "enable-alarms-search": "Įjungti signalų paiešką", + "enable-alarm-filter": "Įjungti signalų filtrą", + "display-alarm-details": "Rodyti signalų detales", + "allow-alarms-ack": "Leisti patvirtinti signalus", + "allow-alarms-clear": "Leisti išvalyti signalus", + "display-alarm-activity": "Rodyti signalų veiklą", + "allow-alarms-assign": "Leisti priskirti signalus", + "columns": "Stulpeliai", + "column-settings": "Stulpelių nustatymai", + "remove-column": "Pašalinti stulpelį", + "add-column": "Pridėti stulpelį", + "no-columns": "Stulpeliai nesukonfigūruoti", + "columns-to-display": "Stulpeliai, kuriuos rodyti", + "table-header": "Lentelės antraštė", + "header-buttons": "Antraštės mygtukai", + "table-buttons": "Lentelės mygtukai", + "pagination": "Puslapiavimas", + "rows": "Eilutės", + "timeseries-column-error": "Turi būti nurodytas bent vienas laiko sekos stulpelis", + "alarm-column-error": "Turi būti nurodytas bent vienas signalų stulpelis", + "table-tabs": "Lentelės skirtukai", + "show-cell-actions-menu-mobile": "Rodyti langelio veiksmų meniu mobiliajame režime", + "disable-sorting": "Išjungti rūšiavimą" }, - "scheduler": { - "scheduler": "Tvarkaraštis", - "scheduler-event": "Tvarkaraščio įvykis", - "select-scheduler-event": "Pasirinkite tvarkaraščio įvykį", - "no-scheduler-events-matching": "Tvarkaraščio įvykių, atitinkančių '{{entity}}' nėra.", - "scheduler-event-required": "Tvarkaraščio įvykis būtinas", - "management": "Tvarkaraščio valdymas", - "scheduler-events": "Tvarkaraščio įvykiai", - "add-scheduler-event": "Pridėti tvarkaraščio įvykį", - "search-scheduler-events": "Tvarkaraščio įvykių paieška", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "type": "Tipas", - "created_customer": "Sukūrė klientas", - "edit-scheduler-event": "Redaguoti tvarkaraščio įvykį", - "view-scheduler-event": "Peržiūrėti tvarkaraščio įvykį", - "delete-scheduler-event": "Panaikinti tvarkaraščio įvykį", - "no-scheduler-events": "Tvarkaraščio įvykių nėra", - "selected-scheduler-events": "Pasirinkta { count, plural, =1 {1 tvarkaraščio įvykis} other {# tvarkaraščio įvykiai} }", - "delete-scheduler-event-title": "Ar tikrai norite panaikinti tvarkaraščio įvykį '{{schedulerEventName}}'?", - "delete-scheduler-event-text": "`", - "delete-scheduler-events-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 tvarkaraščio įvykį} other {# tvarkaraščio įvykius} }?", - "delete-scheduler-events-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti tvarkaraščio įvykiai ir visa su jais susijusi informacija bus panaikinta.", - "create": "Sukurti tvarkaraščio įvykį", - "edit": "Redaguoti tvarkaraščio įvykį", - "view": "Peržiūrėti tvarkaraščio įvykį", - "configuration": "Konfigūracija", - "schedule": "Tvarkaraštis", - "start-time": "Pradžios laikas", - "repeat": "Pakartoti", - "repeats": "Pakartojimai", - "daily": "Kasdieną", - "every-n-days": "Kas N dienas", - "every-n-days-text": "Every { days, plural, =1 {day} other {# days} }", - "weekly": "Kas savaitę", - "every-n-weeks": "Kas N savaites", - "every-n-weeks-text": "Every { weeks, plural, =1 {week} other {# weeks} }", - "monthly": "Kas mėnesį", - "yearly": "Kas metus", - "timer": "Nurodytu laiku", - "repeats-required": "Pakartojimai būtini.", - "repeat-on": "Kartoti", - "repeat-every": "Kartoti kiekvieną", - "repeat-every-n-days": "Kartoti kas N dienas", - "repeat-days-required": "Pakartojim dienų skaičius būtinas.", - "invalid-repeat-days-value": "Pakartojimo dienų skaičius turi būti sveikas teigiamas skaičius.", - "repeat-every-n-weeks": "Kartoti kas N savaites", - "repeat-weeks-required": "Pakartojimo savaičių skaičius būtinas.", - "invalid-repeat-weeks-value": "Pakartojimo savaičių skaičius turi būti sveikas teigiamas skaičius.", - "ends-on": "Pabaiga", - "sunday-label": "Sk", - "monday-label": "Pr", - "tuesday-label": "An", - "wednesday-label": "Tr", - "thursday-label": "Kt", - "friday-label": "Pn", - "saturday-label": "Št", - "repeat-on-sunday": "Kartoti sekmadieniais", - "repeat-on-monday": "Kartoti pirmadieniais", - "repeat-on-tuesday": "Kartoti antradieniais", - "repeat-on-wednesday": "Kartoti trečiadieniais", - "repeat-on-thursday": "Kartoti ketvirtadieniais", - "repeat-on-friday": "Kartoti penktadieniais", - "repeat-on-saturday": "Kartoti šeštadieniais", - "event-type": "Įvykio tipas", - "select-event-type": "Pasirinkite įvykio tiipą", - "event-type-required": "Įvykio tipas būtinas.", - "event-type-max-length": "Įvykio tipas negali viršyti 256 simbolių.", - "list-mode": "Sąrašas", - "calendar-mode": "Kalendorius", - "calendar-view-type": "Kalendoriaus peržiūros tipas", - "month": "Mėnuo", - "week": "Savaitė", - "day": "Diena", - "agenda-week": "Savaitinis tvarkaraštis", - "agenda-day": "Dienų tvarkaraštis", - "list-year": "Sąrašas metams", - "list-month": "Sąrašas mėnesiui", - "list-week": "Sąrašas savaitei", - "list-day": "Sąrašas dienai", - "today": "Šiandien", - "navigate-before": "Ankstesnis", - "navigate-next": "Sekantis", - "starting-from": "Pradėti nuo", - "until": "iki", - "on": "į", - "sunday": "Sekmadienis", - "monday": "Pirmadienis", - "tuesday": "Antradienis", - "wednesday": "Trečiadienis", - "thursday": "Ketvirtadienis", - "friday": "Penktadienis", - "saturday": "Šesštadienis", - "originator": "Iniciatorius", - "single-entity": "Vienas subjektas", - "group-of-entities": "Subjektų grupė", - "entities-group-owner": "Subjektų grupės savininkas", - "single-device": "Vienas įrenginys", - "group-of-devices": "Įrenginių grupė", - "devices-group-owner": "Įrenginių grupės savininkas", - "message-body": "Pranešimo tekstas", - "target": "Tikslas", - "rpc-method": "Method", - "rpc-method-required": "Method is required", - "rpc-method-white-space": "White space is not allowed.", - "rpc-params": "Params", - "select-dashboard-state": "Pasirinkite skydelio būseną", - "hours": "Valandos", - "minutes": "Minutės", - "seconds": "Sekundės", - "time-interval-required": "Laiko intervalas būtinas", - "time-unit-required": "Laiko vienetas būtinas", - "every-hour": "kas { count, plural, =1 {valandą} other {# valandas} }", - "every-minute": "kas { count, plural, =1 {minutę} other {# minutes} }", - "every-second": "kas { count, plural, =1 {sekundę} other {# sekundes} }", - "invalid-time": "Neteisingas laikas" - }, - "report": { - "report-config": "Ataskaitos konfigūracija", - "email-config": "Elektroninio pašto konfigūracija", - "dashboard-state-param": "Skydelio būsenos parametro reikšmė", - "base-url": "Pagrindinis URL", - "base-url-required": "Pagrindinis URL būtinas.", - "use-dashboard-timewindow": "Naudoti skydelio laiko langą", - "timewindow": "Laiko langas", - "name-pattern": "Ataskaitos pavadinimo šablonas", - "name-pattern-required": "Ataskaitos pavadinimo šablonas būtinas", - "type": "Ataskaitos tipas", - "use-current-user-credentials": "Naudoti dabartinio vartotojo įgaliojimus", - "customer-user-credentials": "Kliento vartotojo įgaliojimai", - "customer-user-credentials-required": "Kliento vartotojo įgaliojimai būtini", - "generate-test-report": "Generuoti bandomąją ataskaitą", - "send-email": "Siųsti laišką", + "latest-chart": { + "total": "Bendra suma", + "auto-scale": "Automatinis mastelis", + "clockwise-layout": "Išdėstymas pagal laikrodžio rodyklę", + "sort-series": "Rūšiuoti serijas pagal etiketę", + "tooltip-value-type-absolute": "Absoliuti reikšmė", + "tooltip-value-type-percentage": "Procentinė reikšmė" + }, + "pie-chart": { + "pie-chart-appearance": "Skritulinės diagramos išvaizda", + "label": "Etiketė", + "border": "Kraštinė", + "radius": "Spindulys", + "pie-chart-card-style": "Skritulinės diagramos kortelės stilius" + }, + "radar-chart": { + "radar-appearance": "Radaro diagramos išvaizda", + "shape": "Forma", + "shape-polygon": "Daugiakampis", + "shape-circle": "Apskritimas", + "color": "Spalva", + "line": "Linija", + "points": "Taškai", + "points-label": "Taškų etiketė", + "radar-axis": "Radaro ašis", + "axis-label": "Ašies etiketė", + "ticks-label": "Padalų etiketė", + "radar-chart-style": "Radaro diagramos stilius", + "max-axes-scaling": "Maksimalus ašių mastelis", + "max-axes-scaling-hint": "Pasirinkite, ar kiekviena radaro ašis turi savo maksimalų dydį (Atskiras), ar dalijasi didžiausia reikšme tarp visų ašių pagal valdiklio duomenis (Bendras).", + "separate": "Atskiras", + "common": "Bendras" + }, + "time-series-chart": { + "chart": "Diagrama", + "chart-style": "Diagramos stilius", + "data-zoom": "Duomenų mastelio keitimas", + "stack-mode": "Sluoksniavimo režimas", + "stack-mode-hint": "Sluoksniuoja serijas diagramoje. Serijos su tomis pačiomis vienetais bus dedamos viena ant kitos.", + "axes": "Ašys", + "y-axes": "Y ašys", + "line-type": "Linijos tipas", + "line-width": "Linijos storis", + "type-line": "Linija", + "type-bar": "Stulpelis", + "type-point": "Taškas", + "no-aggregation-bar-width-strategy": "Stulpelių pločio strategija neagreguotiems duomenims", + "no-aggregation-bar-width-strategy-group": "Grupuoti", + "no-aggregation-bar-width-strategy-separate": "Atskiri", + "bar-group-width": "Stulpelių grupės plotis", + "bar-width": "Stulpelio plotis", + "bar-width-relative": "Procentinė laiko lango dalis", + "bar-width-absolute": "Absoliutus (ms)", + "comparison": { + "comparison": "Palyginimas", + "comparison-hint": "Palyginimas veikia tik su istorinių duomenų!", + "show": "Rodyti", + "settings": "Palyginimo nustatymai", + "show-values-for-comparison": "Rodyti istorinius duomenis palyginimui", + "comparison-values-label": "Palyginimo rakto etiketė", + "comparison-values-label-auto": "Automatinė", + "comparison-data-color": "Palyginimo duomenų spalva" + }, + "threshold": { + "thresholds": "Slenksčiai", + "source": "Šaltinis", + "key-value": "Raktas / Reikšmė", + "no-thresholds": "Slenksčiai nesukonfigūruoti", + "add-threshold": "Pridėti slenkstį", + "type-constant": "Pastovus", + "type-latest-key": "Raktas", + "type-entity": "Objektas", + "threshold-settings": "Slenksčio nustatymai", + "remove-threshold": "Pašalinti slenkstį", + "threshold-value-required": "Reikalinga slenksčio reikšmė.", + "key-required": "Reikalingas raktas.", + "entity-key-required": "Reikalingas objekto raktas.", + "line-appearance": "Linijos išvaizda", + "line-color": "Linijos spalva", + "start-symbol": "Pradžios simbolis", + "end-symbol": "Pabaigos simbolis", + "symbol-size": "Dydis", + "label": "Etiketė", + "label-position-start": "Pradžia", + "label-position-middle": "Vidurys", + "label-position-end": "Pabaiga", + "label-position-inside-start": "Viduje pradžioje", + "label-position-inside-start-top": "Viduje viršuje pradžioje", + "label-position-inside-start-bottom": "Viduje apačioje pradžioje", + "label-position-inside-middle": "Viduje per vidurį", + "label-position-inside-middle-top": "Viduje per vidurį viršuje", + "label-position-inside-middle-bottom": "Viduje per vidurį apačioje", + "label-position-inside-end": "Viduje pabaigoje", + "label-position-inside-end-top": "Viduje pabaigoje viršuje", + "label-position-inside-end-bottom": "Viduje pabaigoje apačioje", + "label-background": "Etiketės fonas" + }, + "state": { + "states": "Būsenos", + "label": "Etiketė", + "ticks-value": "Padalų reikšmė", + "source": "Šaltinis", + "value-range": "Reikšmė / Diapazonas", + "no-states": "Būsenos nesukonfigūruotos", + "add-state": "Pridėti būseną", + "type-constant": "Pastovi", + "type-range": "Diapazonas", "from": "Nuo", - "from-required": "Būtina nurodyti nuo ko siunčiamas laiškas.", - "to": "Kam", - "to-required": "Būtina nurodyti kam siunčiamas laiškas.", - "cc": "Cc", - "bcc": "Bcc", - "subject": "Tema", - "subject-required": "Būtina nurodyti laiško temą.", - "body": "Tekstas", - "body-required": "Būtina įrašyti laiško tekstą." - }, - "blob-entity": { - "blob-entity": "Blob subjektas", - "select-blob-entity": "Pasirinkite blob subjektą", - "no-blob-entities-matching": "Blob subjektų, atitinkančių '{{entity}}' nėra.", - "blob-entity-required": "Blob subjektas būtinas", - "files": "Failai", - "search": "Failų paieška", - "clear-search": "Išvalyti paiešką", - "no-blob-entities-prompt": "Failų nėra", - "report": "Ataskaita", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", + "to": "Iki", + "remove-state": "Pašalinti būseną" + }, + "grid": { + "grid": "Tinklelis", + "background-color": "Fono spalva", + "border": "Rėmelis" + }, + "axis": { + "axes": "Ašys", + "x-axis": "X ašis", + "y-axis": "Y ašis", + "y-axis-settings": "Y ašies nustatymai", + "comparison-x-axis-settings": "Palyginimo X ašies nustatymai", + "remove-y-axis": "Pašalinti Y ašį", + "id": "Id", + "label": "Etiketė", + "position": "Pozicija", + "position-left": "Kairėje", + "position-right": "Dešinėje", + "position-top": "Viršuje", + "position-bottom": "Apačioje", + "tick-labels": "Padalų etiketės", + "ticks-formatter-function": "Padalų formatavimo funkcija", + "ticks-generator-function": "Padalų generavimo funkcija", + "show-ticks": "Rodyti padalas", + "show-line": "Rodyti liniją", + "show-split-lines": "Rodyti padalų linijas", + "show-split-lines-x-axis-hint": "Jei įjungta, bus rodomos vertikalios diagramos linijos.", + "show-split-lines-y-axis-hint": "Jei įjungta, bus rodomos horizontalios diagramos linijos.", + "ticks-interval": "Padalų intervalas", + "ticks-interval-hint": "Priverstinai nustato ašies segmentacijos intervalą.", + "split-number": "Padalų skaičius", + "split-number-hint": "Padalų, į kurias suskirstyta ašis, skaičius.", + "min": "Min.", + "max": "Maks.", + "show": "Rodyti", + "add-y-axis": "Pridėti Y ašį" + }, + "series": { + "legend-settings": "Legenda nustatymai", + "show-in-legend": "Rodyti legendoje", + "show-in-legend-hint": "Rodyti serijos pavadinimą ir duomenis legendoje.", + "hidden-by-default": "Numatytai paslėpta", + "hidden-by-default-hint": "Padaryti seriją paslėptą legendoje pagal numatytuosius nustatymus.", + "series-type": "Serijos tipas", "type": "Tipas", - "created_customer": "Skūrė klientas", - "selected-blob-entities": "Pasirinkta{ count, plural, =1 {1 failas} other {# failai} }", - "download-blob-entity": "Atsisiųsti failą", - "delete-blob-entity": "Ištrinti failą", - "delete-blob-entity-title": "Ar tikrai norite ištrinti '{{blobEntityName}}' failą?", - "delete-blob-entity-text": "Būkite dėmesingi, po patvirtinimo failas bus ištrintas.", - "delete-blob-entities-title": "Artikrai norite ištrinti { count, plural, =1 {1 failą} other {# failus} }?", - "delete-blob-entities-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti failai ir su jais susijusi informacija bus ištrinta." - }, - "timezone": { - "timezone": "Laiko juosta", - "select-timezone": "Pasirinkti laiko juostą", - "no-timezones-matching": "Laiko juostų, atitinkančių '{{timezone}}' nėra.", - "timezone-required": "Laiko juosta būtina.", - "browser-time": "Browser Time" - }, - "queue": { - "queue-name": "Queue", - "no-queues-found": "No queues found.", - "no-queues-matching": "No queues matching '{{queue}}' were found.", - "select-name": "Select queue name", - "name": "Name", - "name-required": "Queue name is required!", - "name-unique": "Queue name is not unique!", - "name-pattern": "Queue name contains a character other than ASCII alphanumerics, '.', '_' and '-'!", - "queue-required": "Queue is required!", - "topic-required": "Queue topic is required!", - "poll-interval-required": "Poll interval is required!", - "poll-interval-min-value": "Poll interval value can't be less then 1", - "partitions-required": "Partitions is required!", - "partitions-min-value": "Partitions value can't be less then 1", - "pack-processing-timeout-required": "Processing timeout is required", - "pack-processing-timeout-min-value": "Processing timeout value can't be less then 1", - "batch-size-required": "Batch size is required!", - "batch-size-min-value": "Batch size value can't be less then 1", - "retries-required": "Retries is required!", - "retries-min-value": "Retries value can't be negative", - "failure-percentage-required": "Failure percentage is required!", - "failure-percentage-min-value": "Failure percentage value can't be less then 0", - "failure-percentage-max-value": "Failure percentage value can't be more then 100", - "pause-between-retries-required": "Pause between retries is required!", - "pause-between-retries-min-value": "Pause between retries value can't be less then 1", - "max-pause-between-retries-required": "Max pause between retries is required!", - "max-pause-between-retries-min-value": "Max pause between retries value can't be less then 1", - "submit-strategy-type-required": "Submit strategy type is required!", - "processing-strategy-type-required": "Processing strategy type is required!", - "queues": "Queues", - "selected-queues": "{ count, plural, =1 {1 queue} other {# queues} } selected", - "delete-queue-title": "Are you sure you want to delete the queue '{{queueName}}'?", - "delete-queues-title": "Are you sure you want to delete { count, plural, =1 {1 queue} other {# queues} }?", - "delete-queue-text": "Be careful, after the confirmation the queue and all related data will become unrecoverable.", - "delete-queues-text": "After the confirmation all selected queues will be deleted and won't be accessible.", - "search": "Search queue", - "add": "Add queue", - "details": "Queue details", - "topic": "Topic", - "submit-settings": "Submit settings", - "submit-strategy": "Strategy type *", - "grouping-parameter": "Grouping parameter", - "processing-settings": "Retries processing settings", - "processing-strategy": "Processing type *", - "retries-settings": "Retries settings", - "polling-settings": "Polling settings", - "batch-processing": "Batch processing", - "poll-interval": "Poll interval", - "partitions": "Partitions", - "immediate-processing": "Immediate processing", - "consumer-per-partition": "Send message poll for each consumer", - "consumer-per-partition-hint": "Enable separate consumer(s) per each partition", - "processing-timeout": "Processing within, ms", - "batch-size": "Batch size", - "retries": "Number of retries (0 – unlimited)", - "failure-percentage": "Percentage of failure messages for skipping retries", - "pause-between-retries": "Retry within, sec", - "max-pause-between-retries": "Additional retry within, sec", - "delete": "Delete queue", - "copyId": "Copy queue Id", - "idCopiedMessage": "Queue Id has been copied to clipboard", - "description": "Description", - "description-hint": "This text will be displayed in the Queue description instead of the selected strategy", - "alt-description": "Submit Strategy: {{submitStrategy}}, Processing Strategy: {{processingStrategy}}", - "custom-properties": "Custom properties", - "custom-properties-hint": "Custom queue (topic) creation properties, e.g. 'retention.ms:604800000;retention.bytes:1048576000'", - "strategies": { - "sequential-by-originator-label": "Sequential by originator", - "sequential-by-originator-hint": "New message for e.g. device A is not submitted until previous message for device A is acknowledged", - "sequential-by-tenant-label": "Sequential by tenant", - "sequential-by-tenant-hint": "New message for e.g tenant A is not submitted until previous message for tenant A is acknowledged", - "sequential-label": "Sequential", - "sequential-hint": "New message is not submitted until previous message is acknowledged", - "burst-label": "Burst", - "burst-hint": "All messages are submitted to the rule chains in the order they arrive", - "batch-label": "Batch", - "batch-hint": "New batch is not submitted until previous batch is acknowledged", - "skip-all-failures-label": "Skip all failures", - "skip-all-failures-hint": "Ignore all failures", - "skip-all-failures-and-timeouts-label": "Skip all failures and timeouts", - "skip-all-failures-and-timeouts-hint": "Ignore all failures and timeouts", - "retry-all-label": "Retry all", - "retry-all-hint": "Retry all messages from processing pack", - "retry-failed-label": "Retry failed", - "retry-failed-hint": "Retry all failed messages from processing pack", - "retry-timeout-label": "Retry timeout", - "retry-timeout-hint": "Retry all timed-out messages from processing pack", - "retry-failed-and-timeout-label": "Retry failed and timeout", - "retry-failed-and-timeout-hint": "Retry all failed and timed-out messages from processing pack" + "type-line": "Linija", + "type-bar": "Stulpelis", + "line": { + "line": "Linija", + "show-line": "Rodyti liniją", + "step-line": "Pakopinė linija", + "step-type-start": "Pradžia", + "step-type-middle": "Vidurys", + "step-type-end": "Pabaiga", + "smooth-line": "Glotni linija" + }, + "point": { + "points": "Taškai", + "show-points": "Rodyti taškus", + "point-label": "Taško etiketė", + "point-label-hint": "Rodyti etiketę su reikšme virš serijos taško.", + "point-label-background": "Taško etiketės fonas", + "point-shape": "Taško forma", + "point-size": "Taško dydis" } + } }, - "server-error": { - "general": "General server error", - "authentication": "Authentication error", - "jwt-token-expired": "JWT token expired", - "tenant-trial-expired": "Tenant trial expired", - "credentials-expired": "Credentials expired", - "permission-denied": "Permission denied", - "invalid-arguments": "Invalid arguments", - "bad-request-params": "Bad request params", - "item-not-found": "Item not found", - "too-many-requests": "Too many requests", - "too-many-updates": "Too many updates" - }, - "tenant": { - "tenant": "Valdytojas", - "tenants": "Valdytojai", - "management": "Valdytojų valdymas", - "add": "Pridėti valdytoją", - "admins": "Administratoriai", - "manage-tenant-admins": "Valdyti valdytojo administratorius", - "delete": "Ištrinti valdytoją", - "add-tenant-text": "Pridėti naują valdytoją", - "no-tenants-text": "Valdytojų nėra", - "tenant-details": "Informacija apie valdytoją", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "delete-tenant-title": "Ar tikrai norite ištrinti valdytoją '{{tenantTitle}}'?", - "delete-tenant-text": "Būkite dėmesingi, po patvirtinimo valdytojas ir visa su juo susijusi informacija bus pašalinta.", - "delete-tenants-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }?", - "delete-tenants-action-title": "Pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }", - "delete-tenants-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojai ir su jais susijusi informacija bus pašalinta.", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "description": "Aprašymas", - "details": "Informacija", - "events": "Įvykiai", - "copyId": "Kopijuoti valdytojo Id", - "idCopiedMessage": "Valdytojo Id nukopijuotas į iškarpinę", - "select-tenant": "Pasirinkti valdytoją", - "no-tenants-matching": "Valdytojų, atitinkančių '{{entity}}' nėra.", - "tenant-required": "Valdytojas būtinas", - "allow-white-labeling": "Leisti personalizavimą", - "allow-customer-white-labeling": "Leisti kliento personalizavimą", - "search": "Valdytojų paieška", - "selected-tenants": "Pasirinkta { count, plural, =1 {1 valdytojas} other {# valdutojai} }", - "isolated-tb-rule-engine": "Use isolated ThingsBoard Rule Engine queues", - "isolated-tb-rule-engine-details": "Each tenant will have dedicated Rule Engine queues" - }, - "tenant-profile": { - "tenant-profile": "Valdytojų profilis", - "tenant-profiles": "Valdytojų profiliai", - "add": "Pridėti valdytojų profilį", - "add-profile": "Pridėti profilį", - "edit": "Redaguoti valdytojų profilį", - "tenant-profile-details": "Informacija apie valdytojų profilį", - "no-tenant-profiles-text": "Valdytojų profilių nėra", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "search": "Valdytojų profilių paieška", - "selected-tenant-profiles": "Pasirinkta { count, plural, =1 {1 valdytojų profilis} other {# valdytojų profiliai} }", - "no-tenant-profiles-matching": "Valdytojų profilių, atitinkančių '{{entity}}' nėra.", - "tenant-profile-required": "Valdytojų profilis būtinas", - "idCopiedMessage": "Valdytojų profilio Id nukopijuotas į iškarpinę", - "set-default": "Valdytojų profilį nustatyti kaip pagrndinį", - "delete": "Panaikinti valdytojų profilį", - "copyId": "Kopijuoti valdytojų profilio Id", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "data": "Profilio duomenys", - "profile-configuration": "Profilio konfigūracija", - "description": "Aprašymas", - "default": "Pagrindinis", - "delete-tenant-profile-title": "Ar tikrai norite panaikinti valdytojų profilį '{{tenantProfileName}}'?", - "delete-tenant-profile-text": "Būkite dėmesingi, po patvirtinimo, valdytojų profilis ir visa su juo susijusi informacija bus pašalinta.", - "delete-tenant-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytojų profilį} other {# valdytojų profilius} }?", - "delete-tenant-profiles-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojų profiliai ir su jais susijusi informacija bus pašalinta.", - "set-default-tenant-profile-title": "Ar tikrai norite valdytojų profilį '{{tenantProfileName}}' nustatyti kaip pagrindinį?", - "set-default-tenant-profile-text": "Po patvirtinimo, valdytojų profilis bus nustatytas kaip pagrinfinis ir bus priskiriamas naujiems valdytojams, kuriems valdytojų profilis nenurodytas.", - "no-tenant-profiles-found": "Valdytojų profilių nėra.", - "create-new-tenant-profile": "Sukurti naują!", - "create-tenant-profile": "Sukuri naują valdytojo profilį", - "import": "Importuoti valdytojo profilį", - "export": "Eksportuoti valdytojo profilį", - "export-failed-error": "Valdytojo profilio importuoti nepavyko: {{error}}", - "tenant-profile-file": "Valdytojo profilio failas", - "invalid-tenant-profile-file-error": "Valdytojo profilio importuoti nepavyko: Neteisinga valdytojo profilio duomenų struktūra.", - "advanced-settings": "Pažangūs nustatymai", - "entities": "Subjektai", - "rule-engine": "Rule Engine", - "time-to-live": "Time-to-live", - "alarms-and-notifications": "Alarms and notifications", - "ota-files-in-bytes": "OTA files in bytes", - "ws-title": "WS", - "unlimited": "(0 - unlimited)", - "maximum-devices": "Devices maximum number", - "maximum-devices-required": "Devices maximum number is required.", - "maximum-devices-range": "Devices maximum number can't be negative", - "maximum-assets": "Assets maximum number", - "maximum-assets-required": "Assets maximum number is required.", - "maximum-assets-range": "Assets maximum number can't be negative", - "maximum-customers": "Customers maximum number", - "maximum-customers-required": "Customers maximum number is required.", - "maximum-customers-range": "Customers maximum number can't be negative", - "maximum-users": "Users maximum number", - "maximum-users-required": "Users maximum number is required.", - "maximum-users-range": "Users maximum number can't be negative", - "maximum-dashboards": "Dashboards maximum number", - "maximum-dashboards-required": "Dashboards maximum number is required.", - "maximum-dashboards-range": "Dashboards maximum number can't be negative", - "maximum-edges": "Edges maximum number", - "maximum-edges-required": "Edges maximum number is required.", - "maximum-edges-range": "Edges maximum number can't be negative", - "maximum-rule-chains": "Rule chains maximum number", - "maximum-rule-chains-required": "Rule chains maximum number is required.", - "maximum-rule-chains-range": "Rule chains maximum number can't be negative", - "maximum-integrations": "Integrations maximum number", - "maximum-integrations-required": "Integrations maximum number is required.", - "maximum-integrations-range": "Integrations maximum number can't be negative", - "maximum-converters": "Converters maximum number", - "maximum-converters-required": "Converters maximum number is required.", - "maximum-converters-range": "Converters maximum number can't be negative", - "maximum-scheduler-events": "Scheduler events maximum number", - "maximum-scheduler-events-required": "Scheduler events maximum number is required.", - "maximum-scheduler-events-range": "Scheduler events maximum number can't be negative", - "maximum-resources-sum-data-size": "Resource files sum size", - "maximum-resources-sum-data-size-required": "Resource files sum size is required.", - "maximum-resources-sum-data-size-range": "Resource files sum size can`t be negative", - "maximum-ota-packages-sum-data-size": "OTA package files sum size", - "maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.", - "maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative", - "rest-requests-for-tenant": "REST requests for tenant", - "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", - "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", - "transport-device-msg-rate-limit": "Transport device messages", - "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages", - "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points", - "tenant-entity-export-rate-limit": "Entity version creation", - "tenant-entity-import-rate-limit": "Entity version load", - "tenant-notification-request-rate-limit": "Notification requests", - "tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule", - "integration-msgs-per-tenant-rate-limit": "Tenant integration messages", - "integration-msgs-per-device-rate-limit": "Device integration messages", - "max-transport-messages": "Transport messages maximum number", - "max-transport-messages-required": "Transport messages maximum number is required.", - "max-transport-messages-range": "Transport messages maximum number can't be negative", - "max-transport-data-points": "Transport data points maximum number ", - "max-transport-data-points-required": "Transport data points maximum number is required.", - "max-transport-data-points-range": "Transport data points maximum number can't be negative", - "max-r-e-executions": "Rule Engine executions maximum number", - "max-r-e-executions-required": "Rule Engine executions maximum number is required.", - "max-r-e-executions-range": "Rule Engine executions maximum number can't be negative", - "max-j-s-executions": "JavaScript executions maximum number ", - "max-j-s-executions-required": "JavaScript executions maximum number is required.", - "max-j-s-executions-range": "JavaScript executions maximum number can't be negative", - "max-d-p-storage-days": "Data points storage days maximum number", - "max-d-p-storage-days-required": "Data points storage days maximum number is required.", - "max-d-p-storage-days-range": "Data points storage days maximum number can't be negative", - "default-storage-ttl-days": "Storage TTL days by default", - "default-storage-ttl-days-required": "Storage TTL days by default is required.", - "default-storage-ttl-days-range": "Storage TTL days by default can't be negative", - "alarms-ttl-days": "Alarms TTL days", - "alarms-ttl-days-required": "Alarms TTL days required", - "alarms-ttl-days-days-range": "Alarms TTL days can't be negative", - "rpc-ttl-days": "RPC TTL days", - "rpc-ttl-days-required": "RPC TTL days required", - "rpc-ttl-days-days-range": "RPC TTL days can't be negative", - "max-rule-node-executions-per-message": "Rule node per message executions maximum number", - "max-rule-node-executions-per-message-required": "MRule node per message executions maximum number is required.", - "max-rule-node-executions-per-message-range": "Rule node per message executions maximum number can't be negative", - "max-emails": "Emails sent maximum number", - "max-emails-required": "Emails sent maximum number is required.", - "max-emails-range": "Emails sent maximum number can't be negative", - "sms-enabled": "SMS enabled", - "max-sms": "SMS sent maximum number", - "max-sms-required": "SMS sent maximum number is required.", - "max-sms-range": "SMS sent maximum number can't be negative", - "max-created-alarms": "Alarms created maximum number", - "max-created-alarms-required": "Alarms created maximum number is required.", - "max-created-alarms-range": "Alarms created maximum number be negative", - "no-queue": "No Queue configured", - "add-queue": "Add Queue", - "queues-with-count": "Queues ({{count}})", - "tenant-rest-limits": "REST requests for tenant", - "customer-rest-limits": "REST requests for customer", - "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", - "too-small-value-zero": "The value must be bigger than 0", - "too-small-value-one": "The value must be bigger than 1", - "queue-size-is-limited-by-system-configuration": "The size of the queue is also limited by the system configuration.", - "cassandra-tenant-limits-configuration": "Cassandra query for tenant", - "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number", - "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number", - "ws-limit-max-sessions-per-regular-user": "Sessions per regular user maximum number", - "ws-limit-max-sessions-per-public-user": "Sessions per public user maximum number", - "ws-limit-queue-per-session": "Message queue per session maximum size", - "ws-limit-max-subscriptions-per-tenant": "Subscriptions per tenant maximum number", - "ws-limit-max-subscriptions-per-customer": "Subscriptions per customer maximum number", - "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", - "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", - "ws-limit-updates-per-session": "WS updates per session", - "rate-limits": { - "add-limit": "Add limit", - "advanced-settings": "Advanced settings", - "edit-limit": "Edit limit", - "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", - "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", - "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", - "edit-transport-device-msg-title": "Edit transport device messages rate limits", - "edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits", - "edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits", - "edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits", - "edit-customer-rest-limits-title": "Edit REST requests for customer rate limits", - "edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits", - "edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits", - "edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits", - "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", - "edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits", - "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits", - "edit-integration-msgs-per-tenant-rate-limit-title": "Edit integration messages per tenant rate limits", - "edit-integration-msgs-per-device-rate-limit-title": "Edit integration messages per device rate limits", - "messages-per": "messages per", - "not-set": "Not set", - "number-of-messages": "Number of messages", - "number-of-messages-required": "Number of messages is required.", - "number-of-messages-min": "Minimum value is 1.", - "preview": "Preview", - "per-seconds": "Per seconds", - "per-seconds-required": "Time rate is required.", - "per-seconds-min": "Minimum value is 1.", - "rate-limits": "Rate limits", - "remove-limit": "Remove limit", - "transport-tenant-msg": "Transport tenant messages", - "transport-tenant-telemetry-msg": "Transport tenant telemetry messages", - "transport-tenant-telemetry-data-points": "Transport tenant telemetry data points", - "transport-device-msg": "Transport device messages", - "transport-device-telemetry-msg": "Transport device telemetry messages", - "transport-device-telemetry-data-points": "Transport device telemetry data points", - "sec": "sec" - } + "wind-speed-direction": { + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-advanced": "Išplėstinis", + "layout-simplified": "Supaprastintas", + "values": "Reikšmės", + "wind-direction": "Vėjo kryptis", + "center-value": "Centrinė reikšmė", + "icon": "Piktograma", + "arrow": "Rodyklė", + "ticks": "Padalos", + "labels-type": "Etikečių tipas", + "directional-names": "Krypties pavadinimai", + "degrees": "Laipsniai", + "major-ticks": "Pagrindinės padalos", + "minor-ticks": "Smulkios padalos", + "wind-speed-direction-card-style": "Vėjo greičio ir krypties kortelės stilius", + "ticks-color": "Padalų spalva", + "ticks-labels-type": "Padalų etikečių tipas", + "arrow-color": "Rodyklės spalva" }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, =1 {1 sekundė} other {# sekundės} }", - "minutes-interval": "{ minutes, plural, =1 {1 minutė} other {# minutės} }", - "hours-interval": "{ hours, plural, =1 {1 valanda} other {# valandos} }", - "days-interval": "{ days, plural, =1 {1 diena} other {# dienos} }", - "days": "Dienos", - "hours": "Valandos", - "minutes": "Minutės", - "seconds": "Sekundės", - "advanced": "Patyręs vartotojas", - "predefined": { - "yesterday": "Yesterday", - "day-before-yesterday": "Day before yesterday", - "this-day-last-week": "This day last week", - "previous-week": "Previous week (Sun - Sat)", - "previous-week-iso": "Previous week (Mon - Sun)", - "previous-month": "Previous month", - "previous-quarter": "Previous quarter", - "previous-half-year": "Previous half year", - "previous-year": "Previous year", - "current-hour": "Current hour", - "current-day": "Current day", - "current-day-so-far": "Current day so far", - "current-week": "Current week (Sun - Sat)", - "current-week-iso": "Current week (Mon - Sun)", - "current-week-so-far": "Current week so far (Sun - Sat)", - "current-week-iso-so-far": "Current week so far (Mon - Sun)", - "current-month": "Current month", - "current-month-so-far": "Current month so far", - "current-quarter": "Current quarter", - "current-quarter-so-far": "Current quarter so far", - "current-half-year": "Current half year", - "current-half-year-so-far": "Current half year so far", - "current-year": "Current year", - "current-year-so-far": "Current year so far" - } + "value-source": { + "value-source": "Reikšmės šaltinis", + "predefined-value": "Iš anksto nustatyta reikšmė", + "entity-attribute": "Objekto atributas", + "value": "Reikšmė", + "value-required": "Reikšmė yra privaloma.", + "key-required": "Raktas yra privalomas.", + "entity-key-required": "Objekto raktas yra privalomas.", + "source-entity-alias": "Šaltinio objekto pseudonimas", + "source-entity-attribute": "Šaltinio objekto atributas", + "type-constant": "Pastovus", + "type-latest-key": "Raktas", + "type-entity": "Objektas" }, - "timeunit": { - "milliseconds": "Milisekundės", - "seconds": "Sekundės", - "minutes": "Minutės", - "hours": "Valandos", - "days": "Dienos" - }, - "timewindow": { - "timewindow": "Laikotarpio vaizdas", - "years": "{ years, plural, =1 { metai } other {# metai } }", - "years-short": "{{ years }}m", - "months": "{ months, plural, =1 { mėnuo } other {# mėnesiai } }", - "months-short": "{{ months }}mėn", - "weeks": "{ weeks, plural, =1 { savaitė } other {# savaitės } }", - "weeks-short": "{{ weeks }}sav", - "days": "{ days, plural, =1 { diena } other {# dienos } }", - "days-short": "{{ days }}d", - "hours": "{ hours, plural, =0 { valandų } =1 {1 valanda } other {# valandos } }", - "hr": "{{ hr }} val", - "hr-short": "{{ hr }}val", - "minutes": "{ minutes, plural, =0 { minučių } =1 {1 minutė } other {# minutės } }", - "min": "{{ min }} min", - "min-short": "{{ min }}min", - "seconds": "{ seconds, plural, =0 { sekundžių } =1 {1 sekundė } other {# sekundės } }", - "sec": "{{ sec }} sek", - "sec-short": "{{ sec }}s", - "short": { - "days": "{ days, plural, =1 {1 diena } other {# dienos } }", - "hours": "{ hours, plural, =1 {1 valanda } other {# valandos } }", - "minutes": "{{minutes}} min ", - "seconds": "{{seconds}} sek " - }, - "realtime": "Realiu laiku", - "history": "istorija", - "last-prefix": "Paskutinės", - "period": "nuo {{ startTime }} iki {{ endTime }}", - "edit": "Redaguoti laikotarpio vaizdą", - "date-range": "Datų intervalas", - "for-all-time": "Viskas", - "last": "Paskutinės", - "time-period": "Laikotarpis", - "hide": "Slėpti", - "interval": "Interval", - "just-now": "Just now", - "just-now-lower": "just now", - "ago": "ago", - "style": "Timewindow style", - "icon": "Icon", - "icon-position": "Icon position", - "icon-position-left": "Left", - "icon-position-right": "Right", - "font": "Font", - "color": "Color", - "displayTypePrefix": "Display Realtime/History prefix", - "preview": "Preview" - }, - "unit": { - "millimeter": "Millimeter", - "centimeter": "Centimeter", - "angstrom": "Angstrom", - "nanometer": "Nanometer", - "micrometer": "Micrometer", - "meter": "Meter", - "kilometer": "Kilometer", - "inch": "Inch", - "foot": "Foot", - "yard": "Yard", - "mile": "Mile", - "nautical-mile": "Nautical Mile", - "astronomical-unit": "Astronomical Unit", - "reciprocal-metre": "Reciprocal Metre", - "meter-per-meter": "Meter per meter", - "steradian": "Steradian", - "thou": "Thou", - "barleycorn": "Barleycorn", - "hand": "Hand", - "chain": "Chain", - "furlong": "Furlong", - "league": "League", - "fathom": "Fathom", - "cable": "Cable", - "link": "Link", - "rod": "Rod", - "nanogram": "Nanogram", - "microgram": "Microgram", - "milligram": "Milligram", - "gram": "Gram", - "kilogram": "Kilogram", - "tonne": "Tonne", - "ounce": "Ounce", - "pound": "Pound", - "stone": "Stone", - "hundredweight-count": "Hundredweight count", - "short-tons": "Short tons", - "dalton": "Dalton", - "grain": "Grain", - "drachm": "Drachm", - "quarter": "Quarter", - "slug": "Slug", - "carat": "Carat", - "cubic-millimeter": "Cubic Millimeter", - "cubic-centimeter": "Cubic Centimeter", - "cubic-meter": "Cubic Meter/s", - "cubic-kilometer": "Cubic Kilometers", - "microliter": "Microliter", - "milliliter": "Milliliter", - "liter": "Liter", - "hectoliter": "Hectolitre", - "cubic-inch": "Cubic Inch", - "cubic-foot": "Cubic Foot", - "cubic-yard": "Cubic Yards", - "fluid-ounce": "Fluid Ounce", - "pint": "Pint", - "quart": "Quart", - "gallon": "Gallon", - "oil-barrels": "Oil Barrels", - "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", - "gill": "Gill", - "hogshead": "Hogshead", - "teaspoon": "Teaspoon", - "tablespoon": "Tablespoon", - "cup": "Cup", - "celsius": "Celsius", - "kelvin": "Kelvin", - "rankine": "Rankine", - "fahrenheit": "Fahrenheit", - "percent": "Percent", - "meter-per-second": "Meter per Second", - "kilometer-per-hour": "Kilometer per Hour", - "foot-per-second": "Foot per Second", - "mile-per-hour": "Mile per Hour", - "knot": "Knot", - "millimeters-per-minute": "Millimeters per minute", - "kilometer-per-hour-squared": "Kilometer per hour squared", - "foot-per-second-squared": "Foot per second squared", - "pascal": "Pascal", - "kilopascal": "Kilopascal", - "megapascal": "Megapascal", - "gigapascal": "Gigapascal", - "millibar": "Millibar", - "bar": "Bar", - "kilobar": "Kilobar", - "newton": "Newton", - "newton-meter": "Newton meter", - "foot-pounds": "Foot-pounds", - "inch-pounds": "Inch-pounds", - "newton-per-meter": "Newton per meter", - "atmospheres": "Atmospheres", - "pounds-per-square-inch": "Pounds per Square Inch", - "torr": "Torr", - "inches-of-mercury": "Inches of Mercury", - "pascal-per-square-meter": "Pascal per Square Meter", - "pound-per-square-inch": "Pound per Square Inch", - "newton-per-square-meter": "Newton per Square Meter", - "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", - "pascal-per-square-centimeter": "Pascal per Square Centimeter", - "ton-force-per-square-inch": "Ton-force per Square Inch", - "kilonewton-per-square-meter": "Kilonewton per Square Meter", - "newton-per-square-millimeter": "Newton per Square Millimeter", - "microjoule": "Microjoule", - "millijoule": "Millijoule", - "joule": "Joule", - "kilojoule": "Kilojoule", - "megajoule": "Megajoule", - "gigajoule": "Gigajoule", - "watt-hour": "Watt-hour", - "kilowatt-hour": "Kilowatt-hour", - "electron-volts": "Electron volts", - "joules-per-coulomb": "Joules per Coulomb", - "british-thermal-unit": "British Thermal Units", - "foot-pound": "Foot-pound", - "calorie": "Calorie", - "small-calorie": "Small Calorie", - "kilocalorie": "Kilocalorie", - "joule-per-kelvin": "Joule per Kelvin", - "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", - "joule-per-kilogram": "Joule per Kilogram", - "watt-per-meter-kelvin": "Watt per Meter-Kelvin", - "joule-per-cubic-meter": "Joule per Cubic Meter", - "therm": "Therm", - "electric-dipole-moment": "Electric Dipole Moment", - "magnetic-dipole-moment": "Magnetic Dipole Moment", - "debye": "Debye", - "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", - "milliwatt": "Milliwatt", - "microwatt": "Microwatt", - "watt": "Watt", - "kilowatt": "Kilowatt", - "megawatt": "Megawatt", - "gigawatt": "Gigawatt", - "metric-horsepower": "Metric Horsepower", - "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", - "watt-per-square-centimeter": "Watts per square centimeter", - "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", - "milliwatt-per-square-meter": "Milliwatts per square meter", - "watt-per-square-meter": "Watts per square meter", - "kilowatt-per-square-meter": "Kilowatts per square meter", - "watt-per-square-inch": "Watts per square inch", - "kilowatt-per-square-inch": "Kilowatts per square inch", - "horsepower": "Horsepower", - "btu-per-hour": "British thermal units/hour", - "coulomb": "Coulomb", - "millicoulomb": "Millicoulombs", - "microcoulomb": "Microcoulomb", - "picocoulomb": "Picocoulomb", - "coulomb-per-meter": "Coulomb per meter", - "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", - "coulomb-per-square-meter": "Coulomb per Square Meter", - "square-millimeter": "Square Millimeter", - "square-centimeter": "Square Centimeter", - "square-meter": "Square Meter", - "hectare": "Hectare", - "square-kilometer": "Square Kilometer", - "square-inch": "Square Inch", - "square-foot": "Square Foot", - "square-yard": "Square Yard", - "acre": "Acre", - "square-mile": "Square Mile", - "are": "Are", - "barn": "Barn", - "circular-inch": "Circular Inch", - "milliampere-hour": "Milliampere-hour", - "milliampere-hour-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, milliampere-hour, milliampere-hours, mAh", - "ampere-hours": "Ampere-hours", - "ampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, ampere, ampere-hours, Ah", - "kiloampere-hours": "Kiloampere-hours", - "kiloampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, kiloampere-hours, kiloampere-hour, kAh", - "nanoampere": "Nanoampere", - "nanoampere-tags": "current, amperes, nanoampere, nA", - "picoampere": "Picoampere", - "picoampere-tags": "current, amperes, picoampere, pA", - "microampere": "Microampere", - "microampere-tags": "electric current, microampere, microamperes, μA", - "milliampere": "Milliampere", - "milliampere-tags": "electric current, milliampere, milliamperes, mA", - "ampere": "Ampere", - "ampere-tags": "electric current, current flow, flow of electricity, electrical flow, ampere, amperes, amperage, A", - "kiloamperes": "Kiloamperes", - "kiloamperes-tags": "electric current, current flow, kiloamperes, kA", - "microampere-per-square-centimeter": "Microampere per square centimeter", - "microampere-per-square-centimeter-tags": "Current density, microampere per square centimeter, µA/cm²", - "ampere-per-square-meter": "Ampere per Square Meter", - "ampere-per-square-meter-tags": "current density, current per unit area, ampere per square meter, A/m²", - "ampere-per-meter": "Ampere per Meter", - "ampere-per-meter-tags": "magnetic field strength, magnetic field intensity, ampere per meter, A/m", - "oersted": "Oersted", - "oersted-tags": "magnetic field, oersted, Oe", - "bohr-magneton": "Bohr Magneton", - "bohr-magneton-tags": "atomic physics, magnetic moment, bohr magneton, μB", - "ampere-meter-squared": "Ampere-Meter Squared", - "ampere-meter-squared-tags": "magnetic moment, dipole moment, ampere-meter squared, A·m²", - "ampere-meter": "Ampere-Meter", - "ampere-meter-tags": "magnetic field, current loop, ampere-meter, A·m", - "nanovolt": "Nanovolt", - "picovolt": "Picovolt", - "millivolts": "Millivolts", - "microvolts": "Microvolts", - "volt": "Volt", - "kilovolts": "Kilovolts", - "dbmV": "dBmV", - "volt-meter": "Volt-Meter", - "kilovolt-meter": "Kilovolt-Meter", - "megavolt-meter": "Megavolt-Meter", - "microvolt-meter": "Microvolt-Meter", - "millivolt-meter": "Millivolt-Meter", - "nanovolt-meter": "Nanovolt-Meter", - "ohm": "Ohm", - "microohm": "Microohm", - "milliohm": "Milliohm", - "kilohm": "Kilohm", - "megohm": "Megohm", - "gigohm": "Gigohm", - "hertz": "Hertz", - "kilohertz": "Kilohertz", - "megahertz": "Megahertz", - "gigahertz": "Gigahertz", - "rpm": "Revolutions Per Minute", - "candela-per-square-meter": "Candela per square meter", - "candela": "Candela", - "lumen": "Lumen", - "lux": "Lux", - "foot-candle": "Foot-candle", - "lumen-per-square-meter": "Lumen per square meter", - "lux-second": "Lux second", - "lumen-second": "Lumen second", - "lumens-per-watt": "Lumens per watt", - "absorbance": "Absorbance", - "mole": "Mole", - "nanomole": "Nanomole", - "micromole": "MicroMole", - "millimole": "Millimole", - "kilomole": "Kilomole", - "mole-per-cubic-meter": "Mole per Cubic Meter", - "rssi": "RSSI", - "ppm": "Parts Per Million", - "ppb": "Parts Per Billion", - "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", - "aqi": "AQI", - "gram-per-cubic-meter": "Gram per cubic meter", - "gram-per-kilogram": "Specific Humidity", - "millimeters-per-second": "Millimeters per second", - "neper": "Neper", - "bel": "Bel", - "decibel": "Decibel", - "meters-per-second-squared": "Meters per second squared", - "becquerel": "Becquerel", - "curie": "Curie", - "gray": "Gray", - "sievert": "Sievert", - "roentgen": "Roentgen", - "cps": "Counts per Second", - "rad": "Rad", - "rem": "Rem", - "dps": "Disintegrations per second", - "rutherford": "Rutherford", - "coulombs-per-kilogram": "Coulombs per kilogram", - "becquerels-per-cubic-meter": "Becquerels per cubic meter", - "curies-per-liter": "Curies per liter", - "becquerels-per-second": "Becquerels per second", - "curies-per-second": "Curies per second", - "gy-per-second": "Gray per Second", - "watt-per-steradian": "Watt per Steradian", - "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", - "ph-level": "pH Level", - "turbidity": "Turbidity", - "mg-per-liter": "Milligrams per liter", - "microsiemens-per-centimeter": "Microsiemens per centimeter", - "millisiemens-per-meter": "Millisiemens per meter", - "siemens-per-meter": "Siemens per meter", - "kilogram-per-cubic-meter": "Kilogram per cubic meter", - "gram-per-cubic-centimeter": "Gram per cubic centimeter", - "kilogram-per-square-meter": "Kilogram per square metre", - "milligram-per-milliliter": "Milligram per milliliter", - "pound-per-cubic-foot": "Pound per cubic foot", - "ounces-per-cubic-inch": "Ounces per cubic inch", - "tons-per-cubic-yard": "Tons per cubic yard", - "particle-density": "Particle density", - "kilometers-per-liter": "Kilometers per liter", - "miles-per-gallon": "Miles per gallon", - "liters-per-100-km": "Liters per 100 km", - "gallons-per-mile": "Gallons per mile", - "liters-per-hour": "Liters per hour", - "gallons-per-hour": "Gallons per hour", - "beats-per-minute": "Beats per minute", - "millimeters-of-mercury": "Millimeters of mercury", - "milligrams-per-deciliter": "Milligrams per deciliter", - "g-force": "G-force", - "kilonewton": "Kilonewton", - "kilogram-force": "Kilogram-Force", - "pound-force": "Pound-Force", - "kilopound-force": "Kilopound-Force", - "dyne": "Dyne", - "poundal": "Poundal", - "kip": "Kip", - "gal": "Gal", - "gravity": "Gravity", - "hectopascal": "Hectopascal", - "atmosphere": "Atmosphere", - "millibars": "Millibars", - "inch-of-mercury": "One inch of mercury", - "richter-scale": "Richter Scale", - "second": "Second", - "minute": "Minute", - "hour": "Hour", - "day": "Day", - "week": "Week", - "month": "Month", - "year": "Year", - "cubic-foot-per-minute": "Cubic Foot Per Minute", - "cubic-meters-per-hour": "Cubic Meters Per Hour", - "cubic-meters-per-second": "Cubic Meters Per Second", - "liter-per-second": "Liter Per Second", - "liter-per-minute": "Liter Per Minute", - "gallons-per-minute": "Gallons Per Minute", - "cubic-foot-per-second": "Cubic foot per second", - "milliliters-per-minute": "Milliliters per minute", - "bit": "Bit", - "byte": "Byte", - "kilobyte": "Kilobyte", - "megabyte": "Megabyte", - "gigabyte": "Gigabyte", - "terabyte": "Terabyte", - "petabyte": "Petabyte", - "exabyte": "Exabyte", - "zettabyte": "Zettabyte", - "yottabyte": "Yottabyte", - "bit-per-second": "Bit per second", - "kilobit-per-second": "Kilobit per second", - "megabit-per-second": "Megabit per second", - "gigabit-per-second": "Gigabit per second", - "terabit-per-second": "Terabit per second", - "byte-per-second": "Byte per second", - "kilobyte-per-second": "Kilobyte per second", - "megabyte-per-second": "Megabyte per second", - "gigabyte-per-second": "Gigabyte per second", - "degree": "Degree", - "radian": "Radian", - "gradian": "Gradian", - "mil": "Mil", - "revolution": "Revolution", - "siemens": "Siemens", - "millisiemens": "Millisiemens", - "microsiemens": "Microsiemens", - "kilosiemens": "Kilosiemens", - "megasiemens": "Megasiemens", - "gigasiemens": "Gigasiemens", - "farad": "Farad", - "millifarad": "Millifarad", - "microfarad": "Microfarad", - "nanofarad": "Nanofarad", - "picofarad": "Picofarad", - "kilofarad": "Kilofarad", - "megafarad": "Megafarad", - "gigafarad": "Gigafarad", - "terfarad": "Terfarad", - "farad-per-meter": "Farad per Meter", - "tesla": "Tesla", - "gauss": "Gauss", - "kilogauss": "Kilogauss", - "millitesla": "Millitesla", - "microtesla": "Microtesla", - "nanotesla": "Nanotesla", - "kilotesla": "Kilotesla", - "megatesla": "Megatesla", - "millitesla-square-meters": "millitesla square meters", - "gamma": "Gamma", - "lambda": "Lambda", - "square-meter-per-second": "Square meter per second", - "square-centimeter-per-second": "Square centimeter per second", - "stoke": "Stoke", - "centistokes": "Centistokes", - "square-foot-per-second": "Square foot per second", - "square-inch-per-second": "Square inch per second", - "pascal-second": "Pascal-second", - "centipoise": "Centipoise", - "poise": "Poise", - "reynolds": "Reynolds", - "pound-per-foot-hour": "Pound per foot-hour", - "newton-second-per-square-meter": "Newton second per square meter", - "dyne-second-per-square-centimeter": "Dyne second per square centimeter", - "kilogram-per-meter-second": "Kilogram per meter-second", - "tesla-square-meters": "Tesla square meters", - "maxwell": "Maxwell", - "tesla-per-meter": "Tesla per Meter", - "gauss-per-centimeter": "Gauss per Centimeter", - "weber": "Weber", - "microweber": "Microweber", - "milliweber": "Milliweber", - "gauss-square-centimeter": "Gauss-Square Centimeter", - "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", - "henry": "Henry", - "millihenry": "Millihenry", - "microhenry": "Microhenry", - "nanohenry": "Nanohenry", - "henry-per-meter": "Henry per Meter", - "tesla-meter-per-ampere": "Tesla Meter per Ampere", - "gauss-per-oersted": "Gauss per Oersted", - "kilogram-per-mole": "Kilogram per mole", - "gram-per-mole": "Gram per mole", - "milligram-per-mole": "Milligram per mole", - "joule-per-mole": "Joule per Mole", - "joule-per-mole-kelvin": "Joule per Mole-Kelvin", - "millivolts-per-meter": "Millivolts per meter", - "volts-per-meter": "Volts per meter", - "kilovolts-per-meter": "Kilovolts per meter", - "radian-per-second": "Radian per second", - "radian-per-second-squared": "Radian per second squared", - "revolutions-per-minute-per-second": "Angular acceleration", - "revolutions-per-minute-per-second-squared": "Angular Acceleration", - "deg-per-second": "deg/s", - "degrees-brix": "Degrees Brix", - "katal": "Katal", - "katal-per-cubic-metre": "Katal per Cubic Metre" - }, - "user": { - "all": "Visi", - "all-users": "Visi vartotojai", - "groups": "Grupės", - "user": "Vartotojas", - "users": "Vartotojai", - "management": "Vartotojų valdymas", - "customer-users": "Kliento vartotojai", - "tenant-admins": "Valdytojų administratoriai", - "sys-admin": "Sistemos administratoriai", - "tenant-admin": "Valdytojo administratoriai", - "customer": "Klientas", - "anonymous": "Anonimas", - "add": "Pridėti vartotoją", - "delete": "Panaikinti vartotoją", - "add-user-text": "Pridėti naują vartotoją", - "no-users-text": "Vartotojų nėra", - "user-details": "Informacija apie vartotoją", - "delete-users": "Panaikinti vartotojus", - "delete-user-title": "Ar tikrai norite panaikinti vartotoją '{{userEmail}}'?", - "delete-user-text": "Būkite dėmesingi, po patvirtinimo vartotojas ir visa su juo susijusi informacija bus panaikinta.", - "delete-users-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 vatotoją} other {# vartotojus} }?", - "delete-users-action-title": "Panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }", - "delete-users-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti vartotojai ir su jis susijusi informacija bus panaikinta.", - "activation-email-sent-message": "Aktyvinimo el. laiškas sėkmingai išsiųstas!", - "resend-activation": "Iš naujo siųsti aktyvinimą", - "email": "El. pašto adresas", - "email-required": "El. pašto adresas būtinas.", - "invalid-email-format": "Neteisingas el. pašto adreso formatas.", - "first-name": "Vardas", - "last-name": "Pavardė", - "description": "Aprašymas", - "default-dashboard": "Pagrindinis skydelis", - "always-fullscreen": "Visada per visą ekraną", - "select-user": "Pasirinkti vartotoją", - "no-users-matching": "Vartotojų, atitinkančių'{{entity}}' nėra.", - "user-required": "Vartotojas būtinas", - "activation-method": "Aktyvavimo būdas", - "display-activation-link": "Rodyti aktyvavimo nuorodą", - "send-activation-mail": "Siųsti aktyvavimo el. laišką", - "activation-link": "Vartotojo aktyvavimo nuoroda", - "activation-link-text": "Vartotojui aktyvuoti, paspauskite nuorodą :", - "copy-activation-link": "Kopijuoti aktyvavimo nuorodą", - "activation-link-copied-message": "Vartotojo aktyvavimo nuoroda nukopijuota į iškarpinę", - "details": "Informacija", - "login-as-tenant-admin": "Prisijunkti kaip valdytojo administratorius", - "login-as-customer-user": "Prisijunkti kaip kliento vartotojas", - "select-group-to-add": "Pasirinkite grupę, kuriai priskirti pažymėtus vartotojus", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus vartotojus", - "remove-users-from-group": "Ar tikrai norite pašalinti { count, plural, =1 {1 vartotoją} other {# vartotojus} } iš grupės '{{entityGroup}}'?", - "group": "Vartotojų grupė", - "list-of-groups": "{ count, plural, =1 {Viena vartotojų grupė} other {# Vartotojų grupių sąrašas} }", - "group-name-starts-with": "Vartotojų grupės, kurios prasideda '{{prefix}}'", - "search": "Vartotojų paieška", - "selected-users": "Pasirinkta { count, plural, =1 {1 vartotojas} other {# vartotojai} }", - "disable-account": "Išjungti vartotojo paskyrą", - "enable-account": "Įjungti vartotojo paskyrą", - "enable-account-message": "Vartotojo paskyra sėkmingai įjungta!", - "disable-account-message": "Vartotojo paskyra sėkmingai išjungta!", - "copyId": "Kopijuoti vartotojo Id", - "idCopiedMessage": "Vartotojo Id nukopiuotas į iškarpinę", - "user-list": "Vartotojų sąrašas", - "user-list-required": "Vartotojų sąrašas būtinas" - }, - "value": { - "type": "Reikšmės tipas", - "string": "Tekstas", - "string-value": "Tekstinė informacija", - "string-value-required": "Tekstinė informacija būtina", - "integer": "Sveikasis skaičius", - "integer-value": "Sveikojo skaičiaus reikšmė", - "integer-value-required": "Sveikojo skaičiaus reikšmė būtina", - "invalid-integer-value": "Sveikojo skaičiaus reikšmė neteisinga", - "double": "Realusis skaičius", - "double-value": "Realiojo skaičiaus reikšmė", - "double-value-required": "Realiojo skaičiaus reikšmė būtina", - "boolean": "Loginis", - "boolean-value": "Loginė reikšmė", - "false": "Netiesa", - "true": "Tiesa", - "long": "Sveikas skaičius", - "json": "JSON", - "json-value": "JSON reikšmė", - "json-value-invalid": "Neteisingas JSON reikšmės formatas", - "json-value-required": "JSON reikšmė būtina." - }, - "version-control": { - "version-control": "Version control", - "management": "Version control management", - "search": "Search versions", - "branch": "Branch", - "default": "Default", - "select-branch": "Select branch", - "branch-required": "Branch is required", - "create-entity-version": "Create entity version", - "version-name": "Version name", - "version-name-required": "Version name is required", - "author": "Author", - "export-relations": "Export relations", - "export-attributes": "Export attributes", - "export-credentials": "Export credentials", - "export-group-entities": "Export group entities", - "export-roles": "Export roles", - "entity-versions": "Entity versions", - "versions": "Versions", - "created-time": "Created time", - "version-id": "Version ID", - "no-entity-versions-text": "No entity versions found", - "no-versions-text": "No versions found", - "copy-full-version-id": "Copy full version id", - "create-version": "Create version", - "creating-version": "Creating version... Please wait", - "nothing-to-commit": "No changes to commit", - "restore-version": "Restore version", - "restore-entity-from-version": "Restore entity from version '{{versionName}}'", - "restoring-entity-version": "Restoring entity version... Please wait", - "load-relations": "Load relations", - "load-attributes": "Load attributes", - "load-credentials": "Load credentials", - "load-group-entities": "Load group entities", - "load-roles": "Load roles", - "compare-with-current": "Compare with current", - "diff-entity-with-version": "Diff with entity version '{{versionName}}'", - "previous-difference": "Previous Difference", - "next-difference": "Next Difference", - "current": "Current", - "differences": "{ count, plural, =1 {1 difference} other {# differences} }", - "create-entities-version": "Create entities version", - "default-sync-strategy": "Default sync strategy", - "sync-strategy-merge": "Merge", - "sync-strategy-overwrite": "Overwrite", - "entities-to-export": "Entities to export", - "entities-to-restore": "Entities to restore", - "sync-strategy": "Sync strategy", - "all-entities": "All entities", - "no-entities-to-export-prompt": "Please specify entities to export", - "no-entities-to-restore-prompt": "Please specify entities to restore", - "add-entity-type": "Add entity type", - "remove-all": "Remove all", - "version-create-result": "{ added, plural, =0 {No entities} =1 {1 entity} other {# entities} } added.
    { modified, plural, =0 {No entities} =1 {1 entity} other {# entities} } modified.
    { removed, plural, =0 {No entities} =1 {1 entity} other {# entities} } removed.", - "remove-other-entities": "Remove other entities", - "find-existing-entity-by-name": "Find existing entity by name", - "restore-entities-from-version": "Restore entities from version '{{versionName}}'", - "restoring-entities-from-version": "Restoring entities... Please wait", - "no-entities-restored": "No entities restored", - "created": "{{created}} created", - "updated": "{{updated}} updated", - "deleted": "{{deleted}} deleted", - "groups-created": "{ created, plural, =1 {1 group} other {# groups} } created", - "groups-updated": "{ updated, plural, =1 {1 group} other {# groups} } updated", - "groups-deleted": "{ deleted, plural, =1 {1 group} other {# groups} } deleted", - "remove-other-entities-confirm-text": "Be careful! This will permanently delete all current entities
    not present in the version you want to restore.

    Please type \"remove other entities\" to confirm.", - "auto-commit-to-branch": "auto-commit to {{ branch }} branch", - "default-create-entity-version-name": "{{entityName}} update", - "sync-strategy-merge-hint": "Creates or updates selected entities in the repository. All other repository entities are not modified.", - "sync-strategy-overwrite-hint": "Creates or updates selected entities in the repository. All other repository entities are deleted.", - "device-credentials-conflict": "Failed to load the device with external id {{entityId}}
    due to the same credentials are already present in the database for another device.
    Please consider disabling the load credentials setting in the restore form.", - "missing-referenced-entity": "Failed to load the {{sourceEntityTypeName}} with external id {{sourceEntityId}}
    because it references missing {{targetEntityTypeName}} with id {{targetEntityId}}.", - "add-entity-groups": "Add entity groups", - "entity-groups": "Entity groups", - "integration-routing-key-conflict": "Failed to load the integration with external id {{entityId}}
    due to the same integration key is already present in the database for another integration.
    Please consider enabling the auto-generate integration key setting in the restore form.", - "auto-generate-integration-key": "Auto-generate integration key", - "runtime-failed": "Failed: {{message}}", - "auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings." - }, - "widget": { - "widget-library": "Valdiklių galerija", - "widget-bundle": "Valdiklių rinkinys", - "all-bundles": "Visi rinkiniai", - "select-widgets-bundle": "Pasirinkti valdiklių rinkinį", - "widgets": "Valdikliai", - "all-widgets": "Visi valdikliai", - "widget": "Valdiklis", - "select-widget": "Pasirinkti valdiklį", - "no-widgets-matching": "Valdiklių atitinkančių '{{entity}}' nėra.", - "no-widgets": "Valdiklių dar nėra", - "no-widgets-text": "Valdiklių nėra", - "management": "Valdiklių valdymas", - "editor": "Valdiklių redaktorius", - "widget-type-not-found": "Įkeliant valdiklio konfigūraciją kilo klaidų.
    Gali būti, kad\n toks valdiklio tipas panaikintas.", - "widget-type-load-error": "Valdiklis neįkeltas, nes iškilo klaidų:", - "remove": "Panaikinti valdiklį", - "delete": "Panaikinti valdiklį", - "edit": "Redaguoti valdiklį", - "edit-widget-type": "Redaguoti valdikliį", - "remove-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTitle}}'?", - "remove-widget-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "timeseries": "Telemetrija", - "search-data": "Duomenų paieška", - "no-data-found": "Duomenų nėra", - "latest": "Naujausios reikšmės", - "rpc": "Valdymo valdiklis", - "alarm": "Įspėjimų valdiklis", - "static": "Statinis valdiklis", - "select-widget-type": "Pasirinkite valdiklio tipą", - "missing-widget-title-error": "Valdiklio pavadinimas būtinas!", - "widget-saved": "Valdiklis išsaugotas", - "unable-to-save-widget-error": "Valdiklio išsaugoti negalima! Valdiklyje yra klaidų!", - "save": "Valdiklis išsaugotas", - "saveAs": "valdiklį išsaugoti kaip", - "move": "Perkelti valdiklį", - "save-widget-type-as": "Valdiklį išsaugoti kaip", - "save-widget-type-as-text": "Įrašykite naują valdiklio pavadinimą", - "toggle-fullscreen": "Perjungti į viso ekrano režimą", - "run": "Paleisti valdiklį", - "widget-title": "Pavadinimas", - "title": "Pavadinimas", - "title-required": "Valdiklio pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "system": "Sisteminis", - "type": "Tipas", - "resources": "Resursai", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Panaikinti resursą", - "add-resource": "Pridėti resursą", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Nustatymų schema", - "datakey-settings-schema": "Duomenų raktų nustatymų schema", - "latest-datakey-settings-schema": "Latest data key settings schema", - "widget-settings": "Valdiklio nustatymai", - "description": "Aprašymas", - "image-preview": "Paveiksliuko peržiūra", - "settings-form-selector": "Settings form selector", - "data-key-settings-form-selector": "Data key settings form selector", - "latest-data-key-settings-form-selector": "Latest data key settings form selector", - "all": "All", - "actual": "Actual", - "deprecated": "Pasenęs", - "has-basic-mode": "Has basic mode", - "basic-mode-form-selector": "Basic mode form selector", - "basic-mode": "Basic", - "advanced-mode": "Advanced", - "javascript": "Javascript", - "js": "JS", - "remove-widget-type-title": "Ar tikrai norite pašalinti valdiklį '{{widgetName}}'?", - "remove-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "remove-widget-type": "Panaikinti valdiklį", - "widget-types": "Valdikliai", - "delete-widget-type-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTypeName}}'?", - "delete-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "delete-widget-types-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklį} other {# valdiklius} }?", - "delete-widget-types-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdikliai ir su jais susijusi informacija bus pašalinta.", - "delete-widget-type": "Panaikinti valdiklį", - "add-widget-type": "Pridėti naują valdiklį", - "widget-type-load-failed-error": "Valdiklio įkelti nepavyko!", - "widget-template-load-failed-error": "Valdiklio šablono įkelti nepavyko!", - "details": "Informacija", - "widget-type-details": "Informacija apie valdiklį", - "add": "Pridėti valdiklį", - "no-widget-types-text": "Valdiklių nėra", - "search-widget-types": "Valdiklių paieška", - "selected-widget-types": "Pasirinkta { count, plural, =1 {1 valdiklis} other {# valdikliai} }", - "undo": "Atšaukti valdiklio pakeitimus", - "export": "Eksportuoti valdiklį", - "export-data": "Eksportuoti valdiklio duomenis", - "export-to-csv": "Eksportuoti valdiklio duomenis į CSV...", - "export-to-excel": "Eksportuoti valdiklio duomenis į XLS...", - "export-to-excel-xlsx": "Eksportuoti valdiklio duomenis į XLSX...", - "no-data": "Duomenų atvaizdavimui nėra", - "data-overflow": "Valdiklyje rodoma {{count}} iš {{total}} įrašų", - "alarm-data-overflow": "Valdiklyje rodomi {{allowedEntities}} (didižiausias leistinas skaičius) įspėjimų įrašai iš {{totalEntities}}", - "search": "Valdiklių paieška", - "filter": "Valdiklių filtro tipas", - "loading-widgets": "Valdikliai įkeliami...", - "widget-template-error": "Netinkamas valdiklio HTML šablonas." + "rpc-state": { + "initial-state": "Pradinė būsena", + "initial-state-hint": "Veiksmas, skirtas gauti pradinę komponento būseną (Įjungta / Išjungta).", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Sąlyga, pagal kurią komponentas yra išjungiamas.", + "turn-on": "Įjungti", + "turn-on-hint": "Veiksmas, paleidžiamas, kai jungiklis perjungiamas į 'Įjungta'.", + "turn-off": "Išjungti", + "turn-off-hint": "Veiksmas, paleidžiamas, kai jungiklis perjungiamas į 'Išjungta'.", + "on": "Įjungta", + "off": "Išjungta", + "disabled": "Išjungta" }, - "widget-action": { - "header-button": "Valdiklio antraštės mygtukas", - "open-dashboard-state": "Eiti į naują skydelio būseną", - "update-dashboard-state": "Atnaujinti dabartinę skydelio būseną", - "open-dashboard": "Eiti į kitą skydelio būseną", - "custom": "Aprašomas veiksmas", - "custom-pretty": "Aprašomas veiksmas (su HTML šablonu)", - "custom-pretty-error-title": "Custom dialog error", - "custom-pretty-template-error": "Invalid custom dialog template.", - "custom-pretty-controller-error": "Error occurred while evaluating custom dialog function.", - "mobile-action": "Mobile action", - "target-dashboard-state": "Tikslinė skydelio būsena", - "target-dashboard-state-required": "Tikslnė skydelio būsena būtina", - "set-entity-from-widget": "Nustatyti subjektą iš valdiklio", - "target-dashboard": "Tikslinis skydelis", - "open-right-layout": "Atidarykiti tinkamą skydelio išdėstymą (vaizdas mobiliesiems)", - "state-display-type": "Skydelio būsenos rodymo parinktis", - "open-normal": "Normal", - "open-in-separate-dialog": "Open in separate dialog", - "open-in-popover": "Open in popover", - "dialog-title": "Dialog title", - "dialog-hide-dashboard-toolbar": "Hide dashboard toolbar in dialog", - "dialog-width": "Dialog width in percents relative to viewport width", - "dialog-height": "Dialog height in percents relative to viewport height", - "dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.", - "popover-preferred-placement": "Preferred popover placement", - "popover-placement-top": "Top", - "popover-placement-topLeft": "Top left", - "popover-placement-topRight": "Top right", - "popover-placement-right": "Right", - "popover-placement-rightTop": "Right top", - "popover-placement-rightBottom": "Right bottom", - "popover-placement-bottom": "Bottom", - "popover-placement-bottomLeft": "Bottom left", - "popover-placement-bottomRight": "Bottom right", - "popover-placement-left": "Left", - "popover-placement-leftTop": "Left top", - "popover-placement-leftBottom": "Left bottom", - "popover-hide-on-click-outside": "Hide popover on outside click", - "popover-hide-dashboard-toolbar": "Hide dashboard toolbar in popover", - "popover-width": "Popover width in browser units (ex. 100px, 25vw)", - "popover-height": "Popover height in browser units (ex. 100px, 25vh)", - "popover-style": "Popover style", - "open-new-browser-tab": "Open in a new browser tab", - "mobile": { - "action-type": "Mobile action type", - "action-type-required": "Mobile action type is required", - "take-picture-from-gallery": "Take picture from gallery", - "take-photo": "Take photo", - "map-direction": "Open map directions", - "map-location": "Open map location", - "scan-qr-code": "Scan QR Code", - "make-phone-call": "Make phone call", - "get-location": "Get phone location", - "take-screenshot": "Take screenshot" - } + "value-action": { + "do-nothing": "Nedaryti nieko", + "execute-rpc": "Vykdyti RPC komandą", + "get-attribute": "Gauti atributą", + "set-attribute": "Nustatyti atributą", + "get-time-series": "Gauti laiko eilutę", + "get-alarm-status": "Gauti aliarmo būseną", + "get-dashboard-state": "Gauti skydelio būsenos ID", + "get-dashboard-state-object": "Gauti skydelio būsenos objektą", + "add-time-series": "Pridėti laiko eilutę", + "execute-rpc-text": "Vykdyti RPC metodą '{{methodName}}'", + "get-time-series-text": "Naudoti laiko eilutę '{{key}}'", + "get-attribute-text": "Naudoti atributą '{{key}}'", + "get-alarm-status-text": "Naudoti aliarmo būseną", + "get-dashboard-state-text": "Naudoti skydelio būseną", + "get-dashboard-state-object-text": "Naudoti skydelio būsenos objektą", + "when-dashboard-state-is-text": "Kai skydelio būsena yra '{{state}}'", + "when-dashboard-state-function-is-text": "Kai f(skydelio būsena) = '{{state}}'", + "when-dashboard-state-object-function-is-text": "Kai f(skydelio būsenos objektas) = '{{state}}'", + "set-attribute-to-value-text": "Nustatyti '{{key}}' atributo reikšmę į: {{value}}", + "add-time-series-value-text": "Pridėti '{{key}}' laiko eilutės reikšmę: {{value}}", + "set-attribute-text": "Nustatyti '{{key}}' atributą", + "add-time-series-text": "Pridėti '{{key}}' laiko eilutę", + "action": "Veiksmas", + "value": "Reikšmė", + "init-value-hint": "Reikšmė, kuri bus naudojama iki kol įrenginys atsiųs duomenis.", + "method": "Metodas", + "method-name-required": "Metodo pavadinimas yra privalomas.", + "request-timeout-ms": "RPC užklausos laiko limitas (ms)", + "request-timeout-required": "Reikalingas užklausos laiko limitas.", + "min-request-timeout-error": "Užklausos laiko limitas turi būti ne mažesnis nei 5000 ms (5 sekundės).", + "request-persistent": "Išliekanti RPC užklausa", + "persistent-polling-interval": "Išliekantis apklausos intervalas (ms)", + "persistent-polling-interval-hint": "Apklausos intervalas (ms) išliekantiems RPC komandų atsakymams gauti", + "persistent-polling-interval-required": "Išliekantis apklausos intervalas yra privalomas.", + "min-persistent-polling-interval-error": "Apklausos intervalas turi būti ne mažesnis nei 1000 ms (1 sekundė).", + "attribute-scope": "Atributo taikymo sritis", + "attribute-key": "Atributo raktas", + "attribute-key-required": "Atributo raktas yra privalomas.", + "time-series-key": "Laiko eilutės raktas", + "time-series-key-required": "Laiko eilutės raktas yra privalomas.", + "action-result-converter": "Veiksmo rezultatų konverteris", + "converter-none": "Nėra", + "converter-function": "Funkcija", + "converter-constant": "Pastovi reikšmė", + "converter-value": "Reikšmė", + "parse-value-function": "Reikšmės apdorojimo funkcija", + "state-when-result-is": "'{{state}}' kai rezultatas yra", + "parameters": "Parametrai", + "convert-value-function": "Reikšmės konvertavimo funkcija", + "error": { + "target-entity-is-not-set": "Tikslinis objektas nenustatytas!", + "failed-to-perform-action": "Nepavyko atlikti veiksmo {{ actionLabel }}.", + "invalid-attribute-scope": "{{scope}} atributų taikymo sritis nepalaikoma {{entityType}} objekte." + } }, - "widgets-bundle": { - "current": "Dabartinis rinkinys", - "widgets-bundles": "Valdiklių rinkiniai", - "widgets-bundle-widgets": "Valdiklių rinkinio valdikliai", - "add": "Pridėti valdiklių rinkinį", - "delete": "Panaikinti valdiklių rinkinį", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "image-preview": "Paveiksliuko peržiūra", - "add-widgets-bundle-text": "Pridėti naują valdiklių rinkinį", - "no-widgets-bundles-text": "Valdiklių rinkinių nėra", - "empty": "Valdiklių rinkinys tuščias", - "details": "Informacija", - "widgets-bundle-details": "Informacija apie valdiklių rinkinį", - "delete-widgets-bundle-title": "Ar tikrai norite panaikinti valdiklių rinkinį '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Būkite dėmesingi, po patvirtinimo valdiklių rinkinys ir visa su juo susijusi informacija bus panaikinta.", - "delete-widgets-bundles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }?", - "delete-widgets-bundles-action-title": "Panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinknius} }", - "delete-widgets-bundles-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdiklių rinkiniai ir su jais susijusi informacija bus panaikinta.", - "no-widgets-bundles-matching": "Valdiklių rinkinių, atitinkančių '{{widgetsBundle}}' nėra.", - "widgets-bundle-required": "Valdiklių rinkinys būtinas.", - "system": "Sisteminis", - "import": "Importuoti valdiklių rinknį", - "export": "Eksportuoti valdiklių rinkinį", - "export-widgets-bundle-widgets-prompt": "Valdiklių rinkinių tipus įtraukti į eksportuotus duomenis (kitu atveju bus eksportuojami tik nurodytų valdiklių tipų FQN)", - "export-failed-error": "Valdiklių rinkinio eksportuoti nepavyko: {{error}}", - "create-new-widgets-bundle": "Sukurti naują valdiklių rinkinį", - "widgets-bundle-file": "Valdiklių rinkinio failas", - "invalid-widgets-bundle-file-error": "Valdiklių rinkinio importuoti nepavyko: neteisinga duomenš struktūra.", - "search": "Valdiklių rinkinių paieška", - "selected-widgets-bundles": "Pasirinkta { count, plural, =1 {1 valdiklių rinkinys} other {# valdiklių rinkiniai} }", - "open-widgets-bundle": "Atverti valdiklių rinkinį", - "loading-widgets-bundles": "Įkeliamas valdiklių rinkinys..." - }, - "widget-config": { - "data": "Duomenys", - "settings": "Nustatymai", - "advanced": "Išplėstiniai nustatymai", - "appearance": "Išvaizda", - "widget-card": "Valdiklio kortelė", - "mobile": "Mobilusis vaizdas", - "title": "Pavadinimas", - "title-tooltip": "Pavadinimo paaiškinimas", - "general-settings": "Pagrindiniai nustatymai", - "display-title": "Rodyti valdiklio pavadinimą", - "card-title": "Kortelės pavadinimas", - "drop-shadow": "Šešėlis", - "enable-fullscreen": "Įjungti rodymą per visą ekraną", - "enable-data-export": "Įjungti duomenų eksportą", - "data-export": "Duomenų eksportas", - "background-color": "Fono spalva", - "text-color": "Teksto spalva", - "border-radius": "Rėmelio storis", - "padding": "Padding", - "margin": "Margin", - "widget-style": "Valdiklio stilius", - "widget-css": "Valdiklio CSS", - "title-style": "Antraštės stilius", - "mobile-mode-settings": "Mobiliojo vaizdo režimas", - "order": "Rikiavimo tvarka", - "height": "Aukštis", - "mobile-hide": "Valdiklį paslėpti mobiliojo vaizdo režime", - "desktop-hide": "Slėpti valdiklį darbalaukio režime", - "units": "Papildomas simbolis šalia reikšmės", - "units-by-default": "Numatytieji matavimo vienetai", - "decimals": "Skaimenys po kablelio", - "decimals-by-default": "Dešimtainiai skaičiai pagal numatytuosius nustatymus", - "default-data-key-parameter-hint": "Šis parametras taikomas visoms valdiklių reikšmėms, nebent jis nustatomas duomenų rakto konfigūracijoje", - "units-short": "Vienetai", - "decimals-short": "Dešimtainiai skaičiai", - "decimals-suffix": "dešimtainiai skaičiai", - "timewindow": "Laikotarpio langas", - "use-dashboard-timewindow": "Naudoti skydelio laikotarpio langą", - "use-widget-timewindow": "Naudoti valdiklio laikotarpio langą", - "display-timewindow": "Rodyti laikotarpio langą", - "legend": "Legenda", - "display-legend": "Rodyti legendą", - "datasources": "Duomenų šaltiniai", - "datasource": "Duomenų šaltinis", - "maximum-datasources": "Daugiausiai leidžiama { count, plural, =1 {1 vienas duomenų šaltinis.} other {# duomenų šaltiniai} }", - "timeseries-key-error": "Reikia nurodyti nors vieną telemetrijos rodiklį", - "datasource-type": "Tipas", - "datasource-parameters": "Parametrai", - "remove-datasource": "Pašalinti duomenų šaltinį", - "add-datasource": "Pridėti duomenų šaltinį", - "target-device": "Tikslinis įrenginys", - "alarm-source": "Įspėjimo šaltinis", - "actions": "Veiksmai", - "action": "Veiksmas", - "add-action": "Pridėti veiksmą", - "search-actions": "Veiksmų paieška", - "no-actions-text": "Veiksmų nėra", - "action-source": "Veiksmo šaltinis", - "action-source-required": "Veiksmo šaltinis būtinas.", - "action-name": "Pavadinimas", - "action-name-required": "Veiksmo pavadinimas būtinas.", - "action-name-not-unique": "Veiksmas su tokiu pavadinimu jau yra.
    Šaltinyje veiksmų pavadinimai kartotis negali.", - "action-icon": "Piktograma", - "show-hide-action-using-function": "Rodyti/slėpti veiksmą funkcijos pagalba", - "action-type": "Tipas", - "action-type-required": "Veiksmo tipas būtinas.", - "edit-action": "Redaguoti veiksmą", - "delete-action": "Panaikinti veiksmą", - "delete-action-title": "Panaikinti valdiklio veiksmą", - "delete-action-text": "Ar tikrai norite panaikinti valdiklio veiksmą '{{actionName}}'?", - "title-icon": "Antraštės piktograma", - "display-icon": "Rodyti antraštės piktoramą", - "card-icon": "Kortelės piktograma", - "icon-color": "Piktogramos spalva", - "icon-size": "Piktogramos dydis", - "advanced-settings": "Išplėstiniai nustatymai", - "data-settings": "Duomenų nustatymai", - "limits": "Ribos", - "no-data-display-message": "\"Nėra duomenų\" alternatyvus tekstas", - "data-page-size": "Maximum entities per datasource", - "settings-component-not-found": "Settings form component not found for selector '{{selector}}'", - "preview": "Preview", - "set": "Set", - "set-message": "Set message", - "advanced-title-style": "Advanced title style", - "card-style": "Card style", - "text": "Text", - "background": "Background", - "advanced-widget-style": "Advanced widget style", - "card-buttons": "Card buttons", - "show-card-buttons": "Show card buttons", - "card-border-radius": "Card border radius", - "card-appearance": "Card appearance", - "color": "Color" - }, - "widget-type": { - "import": "Importuoti valdiklį", - "export": "Eksportuoti valdiklį", - "export-widget-types": "Eksportuoti valdiklius", - "export-failed-error": "Valdiklio eksportuoti nepavyko: {{error}}", - "create-new-widget-type": "Sukurt naują valdiklį", - "widget-type-file": "Valdiklių failas", - "invalid-widget-type-file-error": "Nepavyko importuoti valdiklio: neteisinga valdiklio duomenų struktūra." - }, - "self-registration": { - "self-registration": "Self Registration", - "self-registration-url": "Self Registration URL", - "captcha-version": "reCAPTCHA version", - "captcha-action": "reCAPTCHA log action name", - "captcha-site-key": "reCAPTCHA site key", - "captcha-secret-key": "reCAPTCHA secret key", - "notification-email": "El. pašto adresas pranešimų siuntimui", - "notification-email-invalid": "Neteisingas el. pašto adreso formatas.", - "notification-email-required": "El. pašto adresas pranešimų siuntimui būtinas.", - "privacy-policy-text": "Privatumo politikos tekstas", - "terms-of-use-text": "Terms of Use text", - "text-message-page": "Tekstinė žinutė registracijos puslapiui", - "enable-mobile-self-registration": "Enable self registration from mobile application", - "mobile-self-registration-title": "Mobile application self registration settings", - "mobile-package": "Application package", - "mobile-package-placeholder": "Ex.: my.example.app", - "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", - "mobile-package-required": "Application package is required", - "mobile-app-secret": "Application secret", - "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", - "copy-mobile-app-secret": "Copy application secret", - "mobile-app-scheme": "Application URL scheme", - "mobile-app-scheme-placeholder": "Ex.: customscheme", - "mobile-app-scheme-required": "Application URL scheme is required", - "mobile-app-host": "Application URL hostname", - "mobile-app-host-placeholder": "Ex.: my.app.host", - "mobile-app-host-required": "Application URL hostname is required", - "show-privacy-policy": "Show Privacy Policy", - "show-terms-of-use": "Show Terms of Use", - "domain-settings": "Domain settings", - "general-settings": "General settings" - }, - "solution-template": { - "solution-template": "Solution template", - "solution-templates": "Solution templates", - "management": "Manage solution templates", - "details": "Details", - "install": "Install", - "level": "Level", - "install-title": "Solution template successfully installed", - "install-failed-title": "Solution template installation failed", - "instructions": "Instructions", - "goto-main-dashboard": "Goto main dashboard", - "delete": "Delete", - "delete-solution-title": "Are you sure you want to delete the solution '{{solutionTitle}}'?", - "delete-solution-text": "Be careful, after the confirmation the solution and all related data will become unrecoverable.", - "installing": "Installing solution template..." + "widget-font": { + "font-settings": "Šrifto nustatymai", + "font-family": "Šrifto šeima", + "size": "Dydis", + "relative-font-size": "Santykinis šrifto dydis (procentais)", + "font-style": "Stilius", + "font-style-normal": "Normalus", + "font-style-italic": "Kursyvas", + "font-style-oblique": "Pasviręs", + "font-weight": "Storis", + "font-weight-normal": "Normalus", + "font-weight-bold": "Paryškintas", + "font-weight-bolder": "Labiau paryškintas", + "font-weight-lighter": "Plonesnis", + "color": "Spalva", + "shadow-color": "Šešėlio spalva", + "preview": "Peržiūra", + "line-height": "Eilučių aukštis", + "auto": "Automatinis" }, - "markdown": { - "edit": "Edit", - "preview": "Preview", - "copy-code": "Click to copy", - "copied": "Copied!" - }, - "white-labeling": { - "white-labeling": "Tinkinimas", - "white-labeling-general": "General White Labeling", - "login-white-labeling": "Prisijungimo puslapio tinkinimas", - "general": "General", - "login": "Prisijungti", - "preview": "Peržiūra", - "app-title": "Aplikaciijios pavadinimas", - "favicon": "Aplikacijos piktograma", - "favicon-description": "*.ico, *.gif ar *.png paveiksliukas, kurio maksimalus dydis {{kbSize}} kilobaitai.", - "favicon-size-error": "Paveiksliukas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", - "favicon-type-error": "Netinkamas paveiksliuko formatas. Leidžiami tik ICO, GIF ar PNG formatai.", - "drop-favicon-image": "Užvilkite svetainės piktogramos paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-favicon-image-or": "Drag and drop a website icon image or", - "no-favicon-image": "Piktograma nepasirinkta", - "logo": "Logo", - "logo-description": "Bet koks paveiksliukas, kurio dydis neviršija {{kbSize}} kilobaitų.", - "logo-size-error": "Logotipas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", - "logo-type-error": "Netinkamas logotipo failo. Only images are accepted.", - "drop-logo-image": "Užvilkite svetainės logotipo paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-logo-image-or": "Drag and drop a logo image or", - "no-logo-image": "Logotipas nepasirinktas", - "logo-height": "Logotipo aukštis, px", - "primary-palette": "Pirminė paletė", - "accent-palette": "Akcentų paletė", - "customize-palette": "Pritaikyti", - "advanced-css": "Advanced CSS", - "edit-palette": "Redaguoti paletę", - "save-palette": "Išsaugoti paletę", - "primary-background": "Pirminis fonas", - "secondary-background": "Antrinis fonas", - "hue1": "HUE 1", - "hue2": "HUE 2", - "hue3": "HUE 3", - "page-background-color": "Puslapio fono spalva", - "dark-foreground": "Tamsi tema", - "domain-name": "Domeno pavadinimas", - "base-url": "Bazinis URL", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", - "help-link-base-url": "Bazinis pagalbos puslapio url", - "ui-help-base-url": "UI help base url", - "enable-help-links": "Įjungti pagalbos nuorodas", - "error-verification-url": "Domeno pavadinime negali būti '/' ir ':' simbolių. Pavyzdžiui: thingsboard.io", - "show-platform-name-version": "Rodyti platformos pavadinimą ir versiją", - "platform-name": "Platformas pavadinimas", - "platform-version": "Platformos versija", - "version-mask": "{{name}} v.{{version}}", - "position": { - "label": "Platformos pavadinimo ir versijos išdėstymas", - "under-logo": "Po logotipu", - "bottom": "Prisijungimo formos apačioje" - } + "home": { + "no-data-available": "Nėra prieinamų duomenų" }, - "widgets": { - "background": { - "background": "Background", - "background-settings": "Background settings", - "background-type-image": "Upload image", - "background-type-image-url": "Image URL", - "background-type-color": "Solid color", - "image-url": "Image URL", - "overlay": "Overlay", - "enable-overlay": "Enable overlay", - "blur": "Blur", - "preview": "Preview" - }, - "battery-level": { - "layout": "Layout", - "layout-vertical-solid": "Vertical. Solid", - "layout-horizontal-solid": "Horizontal. Solid", - "layout-vertical-divided": "Vertical. Divided", - "layout-horizontal-divided": "Horizontal. Divided", - "icon": "Icon", - "value": "Value", - "auto-scale": "Auto scale", - "battery-level-color": "Battery level color", - "battery-shape-color": "Battery shape color", - "battery-level-card-style": "Battery level card style" - }, - "chart": { - "common-settings": "Common settings", - "enable-stacking-mode": "Enable stacking mode", - "selection": "Time range selection", - "enable-selection-mode": "Enable selection mode", - "line-shadow-size": "Line shadow size", - "display-smooth-lines": "Display smooth (curved) lines", - "default-bar-width": "Default bar width for non-aggregated data (milliseconds)", - "bar-alignment": "Bar alignment", - "bar-alignment-left": "Left", - "bar-alignment-right": "Right", - "bar-alignment-center": "Center", - "default-font": "Default font", - "default-font-size": "Default font size", - "default-font-color": "Default font color", - "thresholds-line-width": "Default line width for all thresholds", - "tooltip-settings": "Tooltip settings", - "tooltip": "Tooltip", - "show-tooltip": "Show tooltip", - "hover-individual-points": "Hover individual points", - "show-cumulative-values": "Show cumulative values in stacking mode", - "hide-zero-false-values": "Hide zero/false values from tooltip", - "tooltip-value-format-function": "Tooltip value format function", - "grid-settings": "Grid settings", - "show-vertical-lines": "Show vertical lines", - "show-horizontal-lines": "Show horizontal lines", - "grid-outline-border-width": "Grid outline/border width (px)", - "primary-color": "Primary color", - "background-color": "Background color", - "ticks-color": "Ticks color", - "xaxis-settings": "X axis settings", - "axis-title": "Axis title", - "xaxis-tick-labels-settings": "X axis tick labels settings", - "show-tick-labels": "Show axis tick labels", - "yaxis-settings": "Y axis settings", - "min-scale-value": "Minimum value on the scale", - "max-scale-value": "Maximum value on the scale", - "yaxis-tick-labels-settings": "Y axis tick labels settings", - "tick-step-size": "Step size between ticks", - "number-of-decimals": "The number of decimals to display", - "ticks-formatter-function": "Ticks formatter function", - "comparison-settings": "Comparison settings", - "enable-comparison": "Enable comparison", - "time-for-comparison": "Comparison period", - "time-for-comparison-previous-interval": "Previous interval (default)", - "time-for-comparison-days": "Day ago", - "time-for-comparison-weeks": "Week ago", - "time-for-comparison-months": "Month ago", - "time-for-comparison-years": "Year ago", - "time-for-comparison-custom-interval": "Custom interval", - "custom-interval-value": "Custom interval value (ms)", - "comparison-x-axis-settings": "Comparison X axis settings", - "axis-position": "Axis position", - "axis-position-top": "Top (default)", - "axis-position-bottom": "Bottom", - "custom-legend-settings": "Custom legend settings", - "enable-custom-legend": "Enable custom legend (this will allow you to use attribute/timeseries values in key labels)", - "key-name": "Key name", - "key-name-required": "Key name is required", - "key-type": "Key type", - "key-type-attribute": "Attribute", - "key-type-timeseries": "Timeseries", - "label-keys-list": "Keys list to use in labels", - "no-label-keys": "No keys configured", - "add-label-key": "Add new key", - "line-width": "Line width", - "color": "Color", - "data-is-hidden-by-default": "Data is hidden by default", - "disable-data-hiding": "Disable data hiding", - "remove-from-legend": "Remove datakey from legend", - "exclude-from-stacking": "Exclude from stacking(available in \"Stacking\" mode)", - "line-settings": "Line settings", - "show-line": "Show line", - "fill-line": "Fill line", - "fill-line-opacity": "Fill opacity", - "points-settings": "Points settings", - "show-points": "Show points", - "points-line-width": "Line width of points", - "points-radius": "Radius of points", - "point-shape": "Point shape", - "point-shape-circle": "Circle", - "point-shape-cross": "Cross", - "point-shape-diamond": "Diamond", - "point-shape-square": "Square", - "point-shape-triangle": "Triangle", - "point-shape-custom": "Custom function", - "point-shape-draw-function": "Point shape draw function", - "show-separate-axis": "Show separate axis", - "axis-position-left": "Left", - "axis-position-right": "Right", - "thresholds": "Thresholds", - "no-thresholds": "No thresholds configured", - "add-threshold": "Add threshold", - "show-values-for-comparison": "Show historical values for comparison", - "comparison-values-label": "Historical values label", - "comparison-line-color": "Comparison line color", - "threshold-settings": "Threshold settings", - "use-as-threshold": "Use key value as threshold", - "threshold-line-width": "Threshold line width", - "threshold-color": "Threshold color", - "common-pie-settings": "Common pie settings", - "radius": "Radius", - "inner-radius": "Inner radius", - "tilt": "Tilt", - "common-pie-settings-range-error": "Value should be in range from 0 to 1", - "stroke-settings": "Stroke settings", - "width-pixels": "Width (pixels)", - "show-labels": "Show labels", - "animation-settings": "Animation settings", - "animated-pie": "Enable pie animation (experimental)", - "border-settings": "Border settings", - "border-width": "Border width", - "border-color": "Border color", - "legend-settings": "Legend settings", - "display-legend": "Display legend", - "labels-font-color": "Labels font color", - "series": "Series", - "add-series": "Add series", - "series-settings": "Series settings", - "remove-series": "Remove series", - "no-series": "No series configured", - "no-series-error": "At least one series should be specified", - "chart-appearance": "Chart appearance", - "vertical-grid-lines": "Vertical grid lines", - "horizontal-grid-lines": "Horizontal grid lines", - "chart-background": "Chart background", - "grid-lines-color": "Grid lines color", - "border": "Border", - "axis": "Axis", - "vertical-axis": "Vertical axis", - "ticks": "Ticks", - "horizontal-axis": "Horizontal axis" - }, - "color": { - "color-settings": "Color settings", - "color-type-constant": "Constant", - "color-type-range": "Range", - "color-type-function": "Function", - "color": "Color", - "value-range": "Value range", - "from": "From", - "to": "To", - "color-function": "Color function" - }, - "dashboard-state": { - "dashboard-state-settings": "Dashboard state settings", - "dashboard-state": "Dashboard state id", - "autofill-state-layout": "Autofill state layout height by default", - "default-margin": "Default widgets margin", - "default-background-color": "Default background color", - "sync-parent-state-params": "Sync state params with parent dashboard" - }, - "date-range-navigator": { - "date-range-picker-settings": "Date range picker settings", - "hide-date-range-picker": "Hide date range picker", - "picker-one-panel": "Date range picker one panel", - "picker-auto-confirm": "Date range picker auto confirm", - "picker-show-template": "Date range picker show template", - "first-day-of-week": "First day of the week", - "interval-settings": "Interval settings", - "hide-interval": "Hide interval", - "initial-interval": "Initial interval", - "interval-hour": "Hour", - "interval-day": "Day", - "interval-week": "Week", - "interval-two-weeks": "2 weeks", - "interval-month": "Month", - "interval-three-months": "3 months", - "interval-six-months": "6 months", - "step-settings": "Step settings", - "hide-step-size": "Hide step size", - "initial-step-size": "Initial step size", - "hide-labels": "Hide labels", - "use-session-storage": "Use session storage", - "localizationMap": { - "Sun": "Sun", - "Mon": "Mon", - "Tue": "Tue", - "Wed": "Wed", - "Thu": "Thu", - "Fri": "Fri", - "Sat": "Sat", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "May", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Oct", - "Nov": "Nov", - "Dec": "Dec", - "January": "January", - "February": "February", - "March": "March", - "April": "April", - "June": "June", - "July": "July", - "August": "August", - "September": "September", - "October": "October", - "November": "November", - "December": "December", - "Custom Date Range": "Custom Date Range", - "Date Range Template": "Date Range Template", - "Today": "Today", - "Yesterday": "Yesterday", - "This Week": "This Week", - "Last Week": "Last Week", - "This Month": "This Month", - "Last Month": "Last Month", - "Year": "Year", - "This Year": "This Year", - "Last Year": "Last Year", - "Date picker": "Date picker", - "Hour": "Hour", - "Day": "Day", - "Week": "Week", - "2 weeks": "2 Weeks", - "Month": "Month", - "3 months": "3 Months", - "6 months": "6 Months", - "Custom interval": "Custom interval", - "Interval": "Interval", - "Step size": "Step size", - "Ok": "Ok" - } - }, - "entities-hierarchy": { - "hierarchy-data-settings": "Hierarchy data settings", - "relations-query-function": "Node relations query function", - "has-children-function": "Node has children function", - "node-state-settings": "Node state settings", - "node-opened-function": "Default node opened function", - "node-disabled-function": "Node disabled function", - "display-settings": "Display settings", - "node-icon-function": "Node icon function", - "node-text-function": "Node text function", - "sort-settings": "Sort settings", - "nodes-sort-function": "Nodes sort function" - }, - "edge": { - "display-default-title": "Display default title" - }, - "gateway": { - "general-settings": "General settings", - "widget-title": "Widget title", - "default-archive-file-name": "Default archive file name", - "device-type-for-new-gateway": "Device type for new gateway", - "messages-settings": "Messages settings", - "save-config-success-message": "Text message about successfully saved gateway configuration", - "device-name-exists-message": "Text message when device with entered name is already exists", - "gateway-title": "Gateway form", - "read-only": "Read only", - "events-title": "Gateway events form title", - "events-filter": "Events filter", - "event-key-contains": "Event key contains...", - "show-connector": "Show for the connector", - "connector-state-param-key": "Connector state parameter key", - "message": "Message", - "created-time": "Created time" - }, - "gauge": { - "default-color": "Default color", - "radial-gauge-settings": "Radial gauge settings", - "ticks-settings": "Ticks settings", - "min-value": "Minimum value", - "max-value": "Maximum value", - "start-ticks-angle": "Start ticks angle", - "ticks-angle": "Ticks angle", - "major-ticks-count": "Major ticks count", - "major-ticks-color": "Major ticks color", - "minor-ticks-count": "Minor ticks count", - "minor-ticks-color": "Minor ticks color", - "tick-numbers-font": "Tick numbers font", - "unit-title-settings": "Unit title settings", - "show-unit-title": "Show unit title", - "unit-title": "Unit title", - "title-font": "Title text font", - "units-settings": "Units settings", - "units-font": "Units text font", - "value-box-settings": "Value box settings", - "show-value-box": "Show value box", - "value-int": "Digits count for integer part of value", - "value-font": "Value text font", - "value-box-rect-stroke-color": "Value box rectangle stroke color", - "value-box-rect-stroke-color-end": "Value box rectangle stroke color - end gradient", - "value-box-background-color": "Value box background color", - "value-box-shadow-color": "Value box shadow color", - "plate-settings": "Plate settings", - "show-plate-border": "Show plate border", - "plate-color": "Plate color", - "needle-settings": "Needle settings", - "needle-circle-size": "Needle circle size", - "needle-color": "Needle color", - "needle-color-end": "Needle color - end gradient", - "needle-color-shadow-up": "Upper half of the needle shadow color", - "needle-color-shadow-down": "Drop shadow needle color", - "highlights-settings": "Highlights settings", - "highlights-width": "Highlights width", - "highlights": "Highlights", - "highlight-from": "From", - "highlight-to": "To", - "highlight-color": "Color", - "no-highlights": "No highlights configured", - "add-highlight": "Add highlight", - "animation-settings": "Animation settings", - "enable-animation": "Enable animation", - "animation-duration": "Animation duration", - "animation-rule": "Animation rule", - "animation-linear": "Linear", - "animation-quad": "Quad", - "animation-quint": "Quint", - "animation-cycle": "Cycle", - "animation-bounce": "Bounce", - "animation-elastic": "Elastic", - "animation-dequad": "Dequad", - "animation-dequint": "Dequint", - "animation-decycle": "Decycle", - "animation-debounce": "Debounce", - "animation-delastic": "Delastic", - "linear-gauge-settings": "Linear gauge settings", - "bar-stroke-width": "Bar stroke width", - "bar-stroke-color": "Bar stroke color", - "bar-background-color": "Gauge bar background color", - "bar-background-color-end": "Bar background color - end gradient", - "progress-bar-color": "Progress bar color", - "progress-bar-color-end": "Progress bar color - end gradient", - "major-ticks-names": "Major ticks names", - "show-stroke-ticks": "Show ticks stroke", - "major-ticks-font": "Major ticks font", - "border-color": "Border color", - "border-width": "Border width", - "needle-circle-color": "Needle circle color", - "animation-target": "Animation target", - "animation-target-needle": "Needle", - "animation-target-plate": "Plate", - "common-settings": "Common gauge settings", - "gauge-type": "Gauge type", - "gauge-type-arc": "Arc", - "gauge-type-donut": "Donut", - "gauge-type-horizontal-bar": "Horizontal bar", - "gauge-type-vertical-bar": "Vertical bar", - "donut-start-angle": "Angle to start from", - "bar-settings": "Gauge bar settings", - "relative-bar-width": "Relative bar width", - "neon-glow-brightness": "Neon glow effect brightness, (0-100), 0 - disable effect", - "stripes-thickness": "Thickness of the stripes, 0 - no stripes", - "rounded-line-cap": "Display rounded line cap", - "bar-color-settings": "Bar color settings", - "use-precise-level-color-values": "Use precise color levels", - "bar-colors": "Bar colors, from lower to upper", - "color": "Color", - "no-bar-colors": "No bar colors configured", - "add-bar-color": "Add bar color", - "from": "From", - "to": "To", - "fixed-level-colors": "Bar colors using boundary values", - "gauge-title-settings": "Gauge title settings", - "show-gauge-title": "Show gauge title", - "gauge-title": "Gauge title", - "gauge-title-font": "Gauge title font", - "unit-title-and-timestamp-settings": "Unit title and timestamp settings", - "show-timestamp": "Show value timestamp", - "timestamp-format": "Timestamp format", - "label-font": "Font of label showing under value", - "value-settings": "Value settings", - "show-value": "Show value text", - "min-max-settings": "Minimum/maximum labels settings", - "show-min-max": "Show min and max values", - "min-max-font": "Font of minimum and maximum labels", - "show-ticks": "Show ticks", - "tick-width": "Tick width", - "tick-color": "Tick color", - "tick-values": "Tick values", - "no-tick-values": "No tick values configured", - "add-tick-value": "Add tick value" - }, - "gpio": { - "pin": "Pin", - "label": "Label", - "row": "Row", - "column": "Column", - "color": "Color", - "panel-settings": "Panel settings", - "background-color": "Background color", - "gpio-switches": "GPIO switches", - "no-gpio-switches": "No GPIO switches configured", - "add-gpio-switch": "Add GPIO switch", - "gpio-status-request": "GPIO status request", - "method-name": "Method name", - "method-body": "Method body", - "gpio-status-change-request": "GPIO status change request", - "parse-gpio-status-function": "Parse gpio status function", - "gpio-leds": "GPIO leds", - "no-gpio-leds": "No GPIO leds configured", - "add-gpio-led": "Add GPIO led" - }, - "html-card": { - "html": "HTML", - "css": "CSS" - }, - "input-widgets": { - "attribute-not-allowed": "Attribute parameter cannot be used in this widget", - "blocked-location": "Geolocation is blocked in your browser", - "claim-device": "Claim device", - "claim-failed": "Failed to claim the device!", - "claim-not-found": "Device not found!", - "claim-successful": "Device was successfully claimed!", - "date": "Date", - "device-name": "Device name", - "device-name-required": "Device name is required", - "discard-changes": "Discard changes", - "entity-attribute-required": "Entity attribute is required", - "entity-coordinate-required": "Both fields, latitude and longitude, are required", - "entity-timeseries-required": "Entity timeseries is required", - "get-location": "Get current location", - "invalid-date": "Invalid Date", - "latitude": "Latitude", - "longitude": "Longitude", - "min-value-error": "Min value is {{value}}", - "max-value-error": "Max value is {{value}}", - "not-allowed-entity": "Selected entity cannot have shared attributes", - "no-attribute-selected": "No attribute is selected", - "no-datakey-selected": "No datakey is selected", - "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", - "no-entity-selected": "No entity selected", - "no-image": "No image", - "no-support-geolocation": "Your browser doesn't support geolocation", - "no-support-web-camera": "Your browser does not support cameras", - "enable-https-use-widget": "Please enable HTTPS to use this widget", - "no-found-your-camera": "Can't find your camera", - "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera", - "no-timeseries-selected": "No timeseries selected", - "secret-key": "Secret key", - "secret-key-required": "Secret key is required", - "switch-attribute-value": "Switch entity attribute value", - "switch-camera": "Switch camera", - "switch-timeseries-value": "Switch entity timeseries value", - "take-photo": "Take photo", - "time": "Time", - "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", - "update-failed": "Update failed", - "update-successful": "Update successful", - "update-attribute": "Update attribute", - "update-timeseries": "Update timeseries", - "value": "Value", - "general-settings": "General settings", - "widget-title": "Widget title", - "claim-button-label": "Claiming button label", - "show-secret-key-field": "Show 'Secret key' input field", - "labels-settings": "Labels settings", - "show-labels": "Show labels", - "device-name-label": "Label for device name input field", - "secret-key-label": "Label for secret key input field", - "messages-settings": "Messages settings", - "claim-device-success-message": "Text message of successful device claiming", - "claim-device-not-found-message": "Text message when device not found", - "claim-device-failed-message": "Text message of failed device claiming", - "claim-device-name-required-message": "'Device name required' error message", - "claim-device-secret-key-required-message": "'Secret key required' error message", - "relations-settings": "Relations settings", - "relate-device-to-state-entity": "Relate device to current state entity", - "relate-direction": "Relate direction", - "relate-direction-from": "From", - "relate-direction-to": "To", - "relation-type": "Relation type", - "show-label": "Show label", - "label": "Label", - "required": "Required", - "required-error-message": "'Required' error message", - "show-result-message": "Show result message", - "integer-field-settings": "Integer field settings", - "min-value": "Min value", - "max-value": "Max value", - "double-field-settings": "Double field settings", - "text-field-settings": "Text field settings", - "min-length": "Min length", - "max-length": "Max length", - "checkbox-settings": "Checkbox settings", - "true-label": "Checked label", - "false-label": "Unchecked label", - "image-input-settings": "Image input settings", - "display-preview": "Display preview", - "display-clear-button": "Display clear button", - "display-apply-button": "Display apply button", - "display-discard-button": "Display discard button", - "datetime-field-settings": "Date/time field settings", - "display-time-input": "Display time input", - "latitude-key-name": "Latitude key name", - "longitude-key-name": "Longitude key name", - "show-get-location-button": "Show button 'Get current location'", - "use-high-accuracy": "Use high accuracy", - "location-fields-settings": "Location fields settings", - "latitude-label": "Label for latitude", - "longitude-label": "Label for longitude", - "input-fields-alignment": "Input fields alignment", - "input-fields-alignment-column": "Column (default)", - "input-fields-alignment-row": "Row", - "layout": "Layout", - "row-gap": "Gap between rows in pixels", - "column-gap": "Gap between columns in pixels", - "latitude-field-required": "Latitude field required", - "longitude-field-required": "Longitude field required", - "attribute-settings": "Attribute settings", - "widget-mode": "Widget mode", - "widget-mode-update-attribute": "Update attribute", - "widget-mode-update-timeseries": "Update timeseries", - "attribute-scope": "Attribute scope", - "attribute-scope-server": "Server attribute", - "attribute-scope-shared": "Shared attribute", - "value-required": "Value required", - "image-settings": "Image settings", - "image-format": "Image format", - "image-format-jpeg": "JPEG", - "image-format-png": "PNG", - "image-format-webp": "WEBP", - "image-quality": "Image quality that use lossy compression such as jpeg and webp", - "max-image-width": "Maximum image width", - "max-image-height": "Maximum image height", - "action-buttons": "Action buttons", - "show-action-buttons": "Show action buttons", - "update-all-values": "Update all values, not only modified", - "save-button-label": "'SAVE' button label", - "reset-button-label": "'UNDO' button label", - "group-settings": "Group settings", - "show-group-title": "Show title for group of fields, related to different entities", - "group-title": "Group title", - "fields-alignment": "Fields alignment", - "fields-alignment-row": "Row (default)", - "fields-alignment-column": "Column", - "fields-in-row": "Number of fields in the row", - "option-value": "Value (write 'null' for create empty option)", - "option-label": "Label", - "hide-input-field": "Hide input field", - "datakey-type": "Datakey type", - "datakey-type-server": "Server attribute (default)", - "datakey-type-shared": "Shared attribute", - "datakey-type-timeseries": "Timeseries", - "datakey-value-type": "Datakey value type", - "datakey-value-type-string": "String", - "datakey-value-type-double": "Double", - "datakey-value-type-integer": "Integer", - "datakey-value-type-json": "JSON", - "datakey-value-type-boolean-checkbox": "Boolean (Checkbox)", - "datakey-value-type-boolean-switch": "Boolean (Switch)", - "datakey-value-type-date-time": "Date & Time", - "datakey-value-type-date": "Date", - "datakey-value-type-time": "Time", - "datakey-value-type-select": "Select", - "datakey-value-type-color": "Color", - "value-is-required": "Value is required", - "ability-to-edit-attribute": "Ability to edit attribute", - "ability-to-edit-attribute-editable": "Editable (default)", - "ability-to-edit-attribute-disabled": "Disabled", - "ability-to-edit-attribute-readonly": "Read-only", - "disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)", - "field-appearance": "Field appearance", - "appearance-fill": "Fill", - "appearance-outline": "Outline", - "subscript-sizing": "Subscript sizing", - "subscript-sizing-fixed": "Fixed", - "subscript-sizing-dynamic": "Dynamic", - "slide-toggle-settings": "Slide toggle settings", - "slide-toggle-label-position": "Slide toggle label position", - "slide-toggle-label-position-after": "After", - "slide-toggle-label-position-before": "Before", - "select-options": "Select options", - "no-select-options": "No select options configured", - "add-select-option": "Add select option", - "numeric-field-settings": "Numeric field settings", - "step-interval": "Step interval between values", - "error-messages": "Error messages", - "min-value-error-message": "'Min value' error message", - "max-value-error-message": "'Max value' error message", - "invalid-date-error-message": "'Invalid date' error message", - "invalid-JSON-error-message": "'Invalid JSON' error message", - "icon-settings": "Icon settings", - "dialog-editor-settings": "Dialog editor settings", - "use-custom-icon": "Use custom icon", - "input-cell-icon": "Icon to show before input cell", - "value-conversion-settings": "Value conversion settings", - "get-value-settings": "Get value settings", - "use-get-value-function": "Use getValue function", - "get-value-function": "getValue function", - "set-value-settings": "Set value settings", - "use-set-value-function": "Use setValue function", - "set-value-function": "setValue function", - "json-invalid": "JSON value has an invalid format", - "title": "Title", - "cancel-button-label": "'Cancel' button label" - }, - "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", - "qr-code": { - "use-qr-code-text-function": "Use QR code text function", - "qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')", - "qr-code-text-pattern-hint": "QR code text pattern use the value of the first found key in the entities in the entity alias.", - "qr-code-text-pattern-required": "QR code text pattern is required.", - "qr-code-text-function": "QR code text function" - }, - "label-widget": { - "label-pattern": "Pattern", - "label-pattern-hint": "Hint: for ex. 'Text ${keyName} units.' or ${#<key index>} units'", - "label-pattern-required": "Pattern is required", - "label-position": "Position (Percentage relative to background)", - "x-pos": "X", - "y-pos": "Y", - "background-color": "Background color", - "font-settings": "Font settings", - "background-image": "Background image", - "labels": "Labels", - "no-labels": "No labels configured", - "add-label": "Add label" - }, - "navigation": { - "title": "Title", - "navigation-path": "Navigation path", - "filter-type": "Filter type", - "filter-type-all": "All items", - "filter-type-include": "Include items", - "filter-type-exclude": "Exclude items", - "items": "Items", - "enter-urls-to-filter": "Enter urls to filter..." - }, - "persistent-table": { - "rpc-id": "RPC ID", - "message-type": "Message type", - "method": "Method", - "params": "Params", - "created-time": "Created time", - "expiration-time": "Expiration time", - "retries": "Retries", - "status": "Status", - "filter": "Filter", - "refresh": "Refresh", - "add": "Add RPC request", - "details": "Details", - "delete": "Delete", - "delete-request-title": "Delete Persistent RPC request", - "delete-request-text": "Are you sure you want to delete request?", - "details-title": "Details RPC ID: ", - "additional-info": "Additional info", - "response": "Response", - "any-status": "Any status", - "rpc-status-list": "RPC status list", - "no-request-prompt": "No request to display", - "send-request": "Send request", - "add-title": "Create Persistent RPC request", - "method-error": "Method is required.", - "timeout-error": "Min timeout value is 5000 (5 seconds).", - "white-space-error": "White space is not allowed.", - "rpc-status": { - "QUEUED": "QUEUED", - "SENT": "SENT", - "DELIVERED": "DELIVERED", - "SUCCESSFUL": "SUCCESSFUL", - "TIMEOUT": "TIMEOUT", - "EXPIRED": "EXPIRED", - "FAILED": "FAILED" - }, - "rpc-search-status-all": "ALL", - "message-types": { - "false": "Two-way", - "true": "One-way" - }, - "general-settings": "General settings", - "enable-filter": "Enable filter", - "enable-sticky-header": "Display header while scrolling", - "enable-sticky-action": "Display actions column while scrolling", - "display-request-details": "Display request details", - "allow-send-request": "Allow send RPC request", - "allow-delete-request": "Allow delete request", - "columns-settings": "Columns settings", - "display-columns": "Columns to display", - "column": "Column", - "no-columns-found": "No columns found", - "no-columns-matching": "'{{column}}' not found." - }, - "rpc": { - "value-settings": "Value settings", - "initial-value": "Initial value", - "retrieve-value-settings": "Retrieve on/off value settings", - "retrieve-value-method": "Retrieve value using method", - "retrieve-value-method-none": "Don't retrieve", - "retrieve-value-method-rpc": "Call RPC get value method", - "retrieve-value-method-attribute": "Subscribe for attribute", - "retrieve-value-method-timeseries": "Subscribe for timeseries", - "attribute-value-key": "Attribute key", - "timeseries-value-key": "Timeseries key", - "get-value-method": "RPC get value method", - "parse-value-function": "Parse value function", - "update-value-settings": "Update value settings", - "set-value-method": "RPC set value method", - "convert-value-function": "Convert value function", - "rpc-settings": "RPC settings", - "request-timeout": "RPC request timeout (ms)", - "persistent-rpc-settings": "Persistent RPC settings", - "request-persistent": "RPC request persistent", - "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", - "common-settings": "Common settings", - "switch-title": "Switch title", - "show-on-off-labels": "Show on/off labels", - "slide-toggle-label": "Slide toggle label", - "label-position": "Label position", - "label-position-before": "Before", - "label-position-after": "After", - "slider-color": "Slider color", - "slider-color-primary": "Primary", - "slider-color-accent": "Accent", - "slider-color-warn": "Warn", - "button-style": "Button style", - "button-raised": "Raised button", - "button-primary": "Primary color", - "button-background-color": "Button background color", - "button-text-color": "Button text color", - "widget-title": "Widget title", - "button-label": "Button label", - "device-attribute-scope": "Device attribute scope", - "server-attribute": "Server attribute", - "shared-attribute": "Shared attribute", - "device-attribute-parameters": "Device attribute parameters", - "is-one-way-command": "Is one way command", - "rpc-method": "RPC method", - "rpc-method-params": "RPC method params", - "show-rpc-error": "Show RPC command execution error", - "led-title": "LED title", - "led-color": "LED color", - "check-status-settings": "Check status settings", - "perform-rpc-status-check": "Perform RPC device status check", - "retrieve-led-status-value-method": "Retrieve led status value using method", - "led-status-value-attribute": "Device attribute containing led status value", - "led-status-value-timeseries": "Device timeseries containing led status value", - "check-status-method": "RPC check device status method", - "parse-led-status-value-function": "Parse led status value function", - "knob-title": "Knob title", - "min-value": "Minimum value", - "max-value": "Maximum value" - }, - "maps": { - "select-entity": "Select entity", - "select-entity-hint": "Hint: after selection click at the map to set position", - "tooltips": { - "placeMarker": "Click to place '{{entityName}}' entity", - "firstVertex": "Polygon for '{{entityName}}': click to place first point", - "firstVertex-cut": "Click to place first point", - "continueLine": "Polygon for '{{entityName}}': click to continue drawing", - "continueLine-cut": "Click to continue drawing", - "finishLine": "Click any existing marker to finish", - "finishPoly": "Polygon for '{{entityName}}': click first marker to finish and save", - "finishPoly-cut": "Click first marker to finish and save", - "finishRect": "Polygon for '{{entityName}}': click to finish and save", - "startCircle": "Circle for '{{entityName}}': click to place circle center", - "finishCircle": "Circle for '{{entityName}}': click to finish circle", - "placeCircleMarker": "Click to place circle marker" - }, - "actions": { - "finish": "Finish", - "cancel": "Cancel", - "removeLastVertex": "Remove last point" - }, - "buttonTitles": { - "drawMarkerButton": "Place entity", - "drawPolyButton": "Create polygon", - "drawLineButton": "Create polyline", - "drawCircleButton": "Create circle", - "drawRectButton": "Create rectangle", - "editButton": "Edit mode", - "dragButton": "Drag-drop mode", - "cutButton": "Cut polygon area", - "deleteButton": "Remove", - "drawCircleMarkerButton": "Create circle marker", - "rotateButton": "Rotate polygon" - }, - "map-provider-settings": "Map provider settings", - "map-provider": "Map provider", - "map-provider-google": "Google maps", - "map-provider-openstreet": "OpenStreet maps", - "map-provider-here": "HERE maps", - "map-provider-image": "Image map", - "map-provider-tencent": "Tencent maps", - "openstreet-provider": "OpenStreet map provider", - "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", - "openstreet-provider-hot": "OpenStreetMap.HOT", - "openstreet-provider-esri-street": "Esri.WorldStreetMap", - "openstreet-provider-esri-topo": "Esri.WorldTopoMap", - "openstreet-provider-esri-imagery": "Esri.WorldImagery", - "openstreet-provider-cartodb-positron": "CartoDB.Positron", - "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", - "use-custom-provider": "Use custom provider", - "custom-provider-tile-url": "Custom provider tile URL", - "google-maps-api-key": "Google Maps API Key", - "default-map-type": "Default map type", - "google-map-type-roadmap": "Roadmap", - "google-map-type-satelite": "Satellite", - "google-map-type-hybrid": "Hybrid", - "google-map-type-terrain": "Terrain", - "map-layer": "Map layer", - "here-map-normal-day": "HERE.normalDay (Default)", - "here-map-normal-night": "HERE.normalNight", - "here-map-hybrid-day": "HERE.hybridDay", - "here-map-terrain-day": "HERE.terrainDay", - "credentials": "Credentials", - "here-app-id": "HERE app id", - "here-app-code": "HERE app code", - "here-api-key": "HERE API key", - "here-use-new-version-api-3": "Use API version 3", - "tencent-maps-api-key": "Tencent Maps API Key", - "tencent-map-type-roadmap": "Roadmap", - "tencent-map-type-satelite": "Satellite", - "tencent-map-type-hybrid": "Hybrid", - "image-map-background": "Image map background", - "image-map-background-from-entity-attribute": "Take image map background from entity attribute", - "image-url-source-entity-alias": "Image URL source entity alias", - "image-url-source-entity-attribute": "Image URL source entity attribute", - "common-map-settings": "Common map settings", - "x-pos-key-name": "X position key name", - "y-pos-key-name": "Y position key name", - "latitude-key-name": "Latitude key name", - "longitude-key-name": "Longitude key name", - "default-map-zoom-level": "Default map zoom level (0 - 20)", - "default-map-center-position": "Default map center position (0,0)", - "disable-scroll-zooming": "Disable scroll zooming", - "disable-double-click-zooming": "Disable double click zooming", - "disable-zoom-control-buttons": "Disable zoom control buttons", - "fit-map-bounds": "Fit map bounds to cover all markers", - "use-default-map-center-position": "Use default map center position", - "entities-limit": "Limit of entities to load", - "markers-settings": "Markers settings", - "marker-offset-x": "Marker X offset relative to position multiplied by marker width", - "marker-offset-y": "Marker Y offset relative to position multiplied by marker height", - "position-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", - "draggable-marker": "Draggable marker", - "label": "Label", - "show-label": "Show label", - "use-label-function": "Use label function", - "label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "label-function": "Label function", - "tooltip": "Tooltip", - "show-tooltip": "Show tooltip", - "show-tooltip-action": "Action for displaying the tooltip", - "show-tooltip-action-click": "Show tooltip on click (Default)", - "show-tooltip-action-hover": "Show tooltip on hover", - "auto-close-tooltips": "Auto-close tooltips", - "use-tooltip-function": "Use tooltip function", - "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "tooltip-function": "Tooltip function", - "tooltip-offset-x": "Tooltip X offset relative to marker anchor multiplied by marker width", - "tooltip-offset-y": "Tooltip Y offset relative to marker anchor multiplied by marker height", - "color": "Color", - "use-color-function": "Use color function", - "color-function": "Color function", - "marker-image": "Marker image", - "use-marker-image-function": "Use marker image function", - "custom-marker-image": "Custom marker image", - "custom-marker-image-size": "Custom marker image size (px)", - "marker-image-function": "Marker image function", - "marker-images": "Marker images", - "polygon-settings": "Polygon settings", - "show-polygon": "Show polygon", - "polygon-key-name": "Polygon key name", - "enable-polygon-edit": "Enable polygon edit", - "polygon-label": "Polygon label", - "show-polygon-label": "Show polygon label", - "use-polygon-label-function": "Use polygon label function", - "polygon-label-pattern": "Polygon label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "polygon-label-function": "Polygon label function", - "polygon-tooltip": "Polygon tooltip", - "show-polygon-tooltip": "Show polygon tooltip", - "auto-close-polygon-tooltips": "Auto-close polygon tooltips", - "use-polygon-tooltip-function": "Use polygon tooltip function", - "polygon-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "polygon-tooltip-function": "Polygon tooltip function", - "polygon-color": "Polygon color", - "polygon-opacity": "Polygon opacity", - "use-polygon-color-function": "Use polygon color function", - "polygon-color-function": "Polygon color function", - "polygon-stroke": "Polygon stroke", - "stroke-color": "Stroke color", - "stroke-opacity": "Stroke opacity", - "stroke-weight": "Stroke weight", - "use-polygon-stroke-color-function": "Use polygon stroke color function", - "polygon-stroke-color-function": "Polygon stroke color function", - "circle-settings": "Circle settings", - "show-circle": "Show circle", - "circle-key-name": "Circle key name", - "enable-circle-edit": "Enable circle edit", - "circle-label": "Circle label", - "show-circle-label": "Show circle label", - "use-circle-label-function": "Use circle label function", - "circle-label-pattern": "Circle label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "circle-label-function": "Circle label function", - "circle-tooltip": "Circle tooltip", - "show-circle-tooltip": "Show circle tooltip", - "auto-close-circle-tooltips": "Auto-close circle tooltips", - "use-circle-tooltip-function": "Use circle tooltip function", - "circle-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "circle-tooltip-function": "Circle tooltip function", - "circle-fill-color": "Circle fill color", - "circle-fill-color-opacity": "Circle fill color opacity", - "use-circle-fill-color-function": "Use circle fill color function", - "circle-fill-color-function": "Circle fill color function", - "circle-stroke": "Circle stroke", - "use-circle-stroke-color-function": "Use circle stroke color function", - "circle-stroke-color-function": "Circle stroke color function", - "markers-clustering-settings": "Markers clustering settings", - "use-map-markers-clustering": "Use map markers clustering", - "zoom-on-cluster-click": "Zoom when clicking on a cluster", - "max-cluster-zoom": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", - "max-cluster-radius-pixels": "Maximum radius that a cluster will cover in pixels", - "cluster-zoom-animation": "Show animation on markers when zooming", - "show-markers-bounds-on-cluster-mouse-over": "Show the bounds of markers when mouse over a cluster", - "spiderfy-max-zoom-level": "Spiderfy at the max zoom level (to see all cluster markers)", - "load-optimization": "Load optimization", - "cluster-chunked-loading": "Use chunks for adding markers so that the page does not freeze", - "cluster-markers-lazy-load": "Use lazy load for adding markers", - "editor-settings": "Editor settings", - "enable-snapping": "Enable snapping to other vertices for precision drawing", - "init-draggable-mode": "Initialize map in draggable mode", - "hide-all-edit-buttons": "Hide all edit control buttons", - "hide-draw-buttons": "Hide draw buttons", - "hide-edit-buttons": "Hide edit buttons", - "hide-remove-button": "Hide remove button", - "route-map-settings": "Route map settings", - "trip-animation-settings": "Trip animation settings", - "normalization-step": "Normalization data step (ms)", - "tooltip-background-color": "Tooltip background color", - "tooltip-font-color": "Tooltip font color", - "tooltip-opacity": "Tooltip opacity (0-1)", - "auto-close-tooltip": "Auto-close tooltip", - "rotation-angle": "Set additional rotation angle for marker (deg)", - "path-settings": "Path settings", - "path-color": "Path color", - "use-path-color-function": "Use path color function", - "path-color-function": "Path color function", - "path-decorator": "Path decorator", - "use-path-decorator": "Use path decorator", - "decorator-symbol": "Decorator symbol", - "decorator-symbol-arrow-head": "Arrow", - "decorator-symbol-dash": "Dash", - "decorator-symbol-size": "Decorator symbol size (px)", - "use-path-decorator-custom-color": "Use path decorator custom color", - "decorator-custom-color": "Decorator custom color", - "decorator-offset": "Decorator offset", - "end-decorator-offset": "End decorator offset", - "decorator-repeat": "Decorator repeat", - "points-settings": "Points settings", - "show-points": "Show points", - "point-color": "Point color", - "point-size": "Point size (px)", - "use-point-color-function": "Use point color function", - "point-color-function": "Point color function", - "use-point-as-anchor": "Use point as anchor", - "point-as-anchor-function": "Point as anchor function", - "independent-point-tooltip": "Independent point tooltip", - "clustering-markers": "Clustering markers", - "use-icon-create-function": "Use markers colour function", - "marker-color-function": "Marker color function" - }, - "markdown": { - "use-markdown-text-function": "Use markdown/HTML value function", - "markdown-text-function": "Markdown/HTML value function", - "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", - "apply-default-markdown-style": "Apply default markdown style", - "markdown-css": "Markdown/HTML CSS" - }, - "simple-card": { - "label": "Label", - "label-position": "Label position", - "label-position-left": "Left", - "label-position-top": "Top" - }, - "value-card": { - "layout": "Layout", - "layout-square": "Square", - "layout-vertical": "Vertical", - "layout-centered": "Centered", - "layout-simplified": "Simplified", - "layout-horizontal": "Horizontal", - "layout-horizontal-reversed": "Horizontal reversed", - "label": "Label", - "icon": "Icon", - "value": "Value", - "date": "Date", - "value-card-style": "Value card style" - }, - "aggregated-value-card": { - "subtitle": "Subtitle", - "chart": "Chart", - "values": "Values", - "value-appearance": "Value appearance", - "position": "Position", - "position-center": "Center", - "position-right-top": "Right top", - "position-right-bottom": "Right bottom", - "position-left-top": "Left top", - "position-left-bottom": "Left bottom", - "font": "Font", - "color": "Color", - "arrow": "Arrow", - "display-up-down-arrow": "Display Up/Down arrow", - "add-value": "Add value", - "remove-value": "Remove value", - "no-values": "No values configured", - "aggregation": "Aggregation", - "aggregated-value-card-style": "Aggregated value card style" - }, - "alarm-count": { - "alarm-count-card-style": "Alarm count card style" - }, - "entity-count": { - "entity-count-card-style": "Entity count card style" - }, - "count": { - "layout": "Layout", - "layout-column": "Column", - "layout-row": "Row", - "label": "Label", - "icon": "Icon", - "icon-background": "Icon background", - "value": "Value", - "chevron": "Chevron" - }, - "table": { - "common-table-settings": "Common Table Settings", - "enable-search": "Enable search", - "enable-sticky-header": "Always display header", - "enable-sticky-action": "Always display actions column", - "hidden-cell-button-display-mode": "Hidden cell button actions display mode", - "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", - "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", - "display-timestamp": "Display timestamp column", - "display-milliseconds": "Display timestamp milliseconds", - "display-pagination": "Display pagination", - "default-page-size": "Default page size", - "use-entity-label-tab-name": "Use entity label in tab name", - "hide-empty-lines": "Hide empty lines", - "row-style": "Row style", - "use-row-style-function": "Use row style function", - "row-style-function": "Row style function", - "cell-style": "Cell style", - "use-cell-style-function": "Use cell style function", - "cell-style-function": "Cell style function", - "cell-content": "Cell content", - "use-cell-content-function": "Use cell content function", - "cell-content-function": "Cell content function", - "show-latest-data-column": "Show latest data column", - "latest-data-column-order": "Latest data column order", - "entities-table-title": "Entities table title", - "enable-select-column-display": "Enable select columns to display", - "display-entity-name": "Display entity name column", - "entity-name-column-title": "Entity name column title", - "display-entity-label": "Display entity label column", - "entity-label-column-title": "Entity label column title", - "display-entity-type": "Display entity type column", - "default-sort-order": "Default sort order", - "custom-title": "Custom header title", - "column-width": "Column width (px or %)", - "default-column-visibility": "Default column visibility", - "column-visibility-visible": "Visible", - "column-visibility-hidden": "Hidden", - "column-visibility-hidden-mobile": "Hidden in mobile mode", - "column-selection-to-display": "Column selection in 'Columns to Display'", - "column-selection-to-display-enabled": "Enabled", - "column-selection-to-display-disabled": "Disabled", - "column-export-option": "Include column in export", - "column-export-option-always": "Always", - "column-export-option-only-visible": "Only if column visible", - "column-export-option-never": "Never", - "alarms-table-title": "Alarms table title", - "enable-alarms-selection": "Enable alarms selection", - "enable-alarms-search": "Enable alarms search", - "enable-alarm-filter": "Enable alarm filter", - "display-alarm-details": "Display alarm details", - "allow-alarms-ack": "Allow alarms acknowledgment", - "allow-alarms-clear": "Allow alarms clear", - "display-alarm-activity": "Display alarm activity", - "allow-alarms-assign": "Allow alarms assignment", - "blob-entities-table-title": "Blob entities table title", - "display-created-time": "Display created time column", - "display-type": "Display type column", - "display-customer": "Display customer column", - "no-data-display-message": "\"No data to display\" alternative message", - "force-default-blob-entity-type": "Force default blob entity type", - "scheduler-events-table-title": "Scheduler events table title", - "force-default-event-type": "Force default event type", - "columns": "Columns", - "column-settings": "Column settings", - "remove-column": "Remove column", - "add-column": "Add column", - "no-columns": "No columns configured", - "columns-to-display": "Columns to display", - "table-header": "Table header", - "header-buttons": "Header buttons", - "table-buttons": "Table buttons", - "pagination": "Pagination", - "rows": "Rows", - "timeseries-column-error": "At least one timeseries column should be specified", - "table-tabs": "Table tabs", - "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" - }, - "scheduler": { - "enabled-scheduler-event-views": "Enabled scheduler events views", - "enabled-scheduler-event-views-both": "Both", - "enabled-scheduler-event-views-list": "List View Only", - "enabled-scheduler-event-views-calendar": "Calendar View Only", - "display-name": "Display name", - "type-name": "Type name", - "display-originator-select": "Display originator entity select", - "display-message-type-select": "Display message type select", - "display-metadata-table": "Display message metadata table", - "configuration-html-template": "Configuration HTML template", - "custom-event-types": "Custom event types", - "no-custom-event-types": "No custom event types configured", - "add-custom-event-type": "Add custom event type" - }, - "value-source": { - "value-source": "Value source", - "predefined-value": "Predefined value", - "entity-attribute": "Entity attribute", - "value": "Value", - "source-entity-alias": "Source entity alias", - "source-entity-attribute": "Source entity attribute" - }, - "widget-font": { - "font-settings": "Font settings", - "font-family": "Font family", - "size": "Size", - "relative-font-size": "Relative font size (percents)", - "font-style": "Style", - "font-style-normal": "Normal", - "font-style-italic": "Italic", - "font-style-oblique": "Oblique", - "font-weight": "Weight", - "font-weight-normal": "Normal", - "font-weight-bold": "Bold", - "font-weight-bolder": "Bolder", - "font-weight-lighter": "Lighter", - "color": "Color", - "shadow-color": "Shadow color", - "preview": "Preview", - "line-height": "Line height", - "auto": "Auto" - }, - "home": { - "no-data-available": "No data available" - }, - "system-info": { - "cpu": "CPU", - "ram": "RAM", - "disk": "Disk", - "cpu-warning-text": "Running high on CPU usage. To avoid system failure, optimize system performance.", - "cpu-critical-text": "Critically high CPU usage. To avoid system failure, optimize system performance.", - "ram-warning-text": "Running low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", - "ram-critical-text": "Critically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", - "disk-warning-text": "Running low on disk space. To avoid data loss, free up or expand the disk space.", - "disk-critical-text": "Critically low disk space. To avoid data loss, free up or expand the disk space." - }, - "cluster-info": { - "service-id": "Service id", - "service-type": "Service type", - "no-data": "No data" - }, - "transport-messages": { - "title": "Transport messages", - "info": "All the messages that came from devices" - }, - "activity": { - "title": "Activity" - }, - "documentation": { - "title": "Documentation", - "add-link": "Add link", - "add-link-title": "Add documentation link", - "name": "Name", - "name-required": "Name is required.", - "link": "Link", - "link-required": "Link is required.", - "columns": "Columns" - }, - "quick-links": { - "title": "Quick links", - "add-link": "Add link", - "add-link-title": "Add quick link", - "quick-link": "Quick link", - "quick-link-required": "Quick link is required.", - "no-links-matching": "No links matching '{{name}}' were found.", - "columns": "Columns" - }, - "recent-dashboards": { - "title": "Dashboards", - "last": "Last viewed", - "starred": "Starred", - "name": "Name", - "last-viewed": "Last viewed", - "no-last-viewed-dashboards": "No last viewed dashboards yet" + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Diskas", + "cpu-warning-text": "Didelis procesoriaus apkrovimas. Siekiant išvengti sistemos gedimo, optimizuokite našumą.", + "cpu-critical-text": "Kritiškai didelis procesoriaus apkrovimas. Nedelsiant optimizuokite sistemos našumą.", + "ram-warning-text": "Mažas RAM rezervas. Padidinkite RAM arba optimizuokite sistemos veikimą.", + "ram-critical-text": "Kritiškai mažas RAM rezervas. Būtina optimizacija arba RAM padidinimas.", + "disk-warning-text": "Mažai vietos diske. Išvalykite arba išplėskite disko talpą.", + "disk-critical-text": "Kritiškai mažai vietos diske. Skubiai atlaisvinkite arba išplėskite talpą." + }, + "cluster-info": { + "service-id": "Paslaugos ID", + "service-type": "Paslaugos tipas", + "no-data": "Nėra duomenų" + }, + "transport-messages": { + "title": "Transporto pranešimai", + "info": "Visi pranešimai, gauti iš įrenginių" + }, + "activity": { + "title": "Aktyvumas" + }, + "documentation": { + "title": "Dokumentacija", + "add-link": "Pridėti nuorodą", + "add-link-title": "Pridėti dokumentacijos nuorodą", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "link": "Nuoroda", + "link-required": "Nuoroda yra privaloma.", + "columns": "Stulpeliai" + }, + "quick-links": { + "title": "Greitosios nuorodos", + "add-link": "Pridėti nuorodą", + "add-link-title": "Pridėti greitąją nuorodą", + "quick-link": "Greitoji nuoroda", + "quick-link-required": "Greitoji nuoroda yra privaloma.", + "no-links-matching": "Nuorodų, atitinkančių '{{name}}', nerasta.", + "columns": "Stulpeliai" + }, + "recent-dashboards": { + "title": "Valdymo skydeliai", + "last": "Paskutiniai peržiūrėti", + "starred": "Pažymėti žvaigždute", + "name": "Pavadinimas", + "last-viewed": "Paskutinį kartą peržiūrėtas", + "no-last-viewed-dashboards": "Dar nėra peržiūrėtų skydelių" + }, + "configured-features": { + "title": "Sukonfigūruotos funkcijos", + "info": "Funkcijų, kurioms reikia konfigūracijos, būsena", + "email-feature": "El. paštas", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "Dvigubas autentifikavimas (2FA)", + "feature-configured": "Funkcija sukonfigūruota.\nSpustelėkite, kad nustatytumėte", + "feature-not-configured": "Funkcija nesukonfigūruota.\nSpustelėkite, kad nustatytumėte" + }, + "version-info": { + "title": "Versija", + "contact-us": "Susisiekite su mumis", + "current-version": "Dabartinė versija", + "current": "Dabartinė", + "available-version": "Prieinama versija", + "available": "Prieinama", + "upgrade": "Atnaujinti", + "version-is-up-to-date": "Versija yra naujausia" + }, + "usage-info": { + "title": "Naudojimas", + "entities": "Objektai", + "api-calls": "API užklausos" + }, + "functions": { + "title": "Funkcijos", + "pe-feature-tooltip": "Galima tik ThingsBoard\nProfessional Edition versijoje", + "switch-to-pe": "Pereiti į PE versiją", + "alarms": "Aliarmai", + "dashboards": "Valdymo skydeliai", + "entities-and-relations": "Objektai ir ryšiai", + "profiles": "Profiliai", + "advanced-features": "Išplėstinės funkcijos", + "notification-center": "Pranešimų centras", + "api-usage": "API naudojimas", + "customers": "Klientai", + "customers-hierarchy": "Klientų hierarchija", + "roles-and-permissions": "Vaidmenys ir leidimai", + "groups": "Grupės", + "integrations": "Integracijos", + "solution-templates": "Sprendimų šablonai", + "scheduler": "Tvarkaraštis", + "white-labeling": "Prekinio ženklo pritaikymas (White-labeling)" + }, + "devices": { + "view-docs": "Peržiūrėti dokumentaciją", + "inactive": "Neaktyvūs", + "active": "Aktyvūs", + "total": "Iš viso" + }, + "alarms": { + "critical": "Kritiniai", + "assigned-to-me": "Priskirti man", + "total": "Iš viso" + }, + "getting-started": { + "get-started": "Pradėti", + "finish": "Baigti", + "done-welcome-title": "Sveiki atvykę!", + "done-welcome-text": "Puikiai susitvarkėte!", + "sys-admin": { + "step1": { + "title": "Sukurkite nuomininką ir jo administratorių", + "content": "

    Nuomininkas – tai fizinis ar juridinis asmuo, turintis arba valdantis įrenginius ir išteklius. Nuomininkas gali turėti kelis administratoriaus vartotojus, klientus, įrenginius ir išteklius.

    Nuomininko administratorius gali kurti ir valdyti įrenginius, išteklius, klientus bei valdymo skydelius nuomininko paskyroje.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-tenant": "Kaip sukurti nuomininką ir jo administratorių" }, - "configured-features": { - "title": "Configured features", - "info": "Status of features that require configuration", - "white-labeling-feature": "White labeling", - "email-feature": "Email", - "sms-feature": "SMS", - "slack-feature": "Slack", - "oauth2-feature": "OAuth 2", - "2fa-feature": "2FA", - "feature-configured": "Feature is configured.\nClick to setup", - "feature-not-configured": "Feature is not configured.\nClick to setup" + "step2": { + "title": "Sukonfigūruokite funkciją: Pašto serveris", + "content": "

    Pašto serverio konfigūracija būtina vartotojų aktyvavimui, slaptažodžio atkūrimui ir aliarmų pranešimų siuntimui.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-mail-server": "Kaip sukonfigūruoti pašto serverį" }, - "version-info": { - "title": "Version", - "contact-us": "Contact us", - "current-version": "Current version", - "current": "Current", - "available-version": "Available version", - "available": "Available", - "upgrade": "Upgrade", - "version-is-up-to-date": "Version is up to date" + "step3": { + "title": "Sukonfigūruokite funkciją: SMS tiekėjas", + "content": "

    Sukonfigūruokite SMS tiekėjus, kad klientai gautų aliarmų pranešimus SMS žinutėmis.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-sms-provider": "Kaip sukonfigūruoti SMS tiekėją" }, - "license-info": { - "title": "License info", - "view-all": "View all", - "license-portal": "License portal", - "devices-info": "{count} / { max, plural, =0 {∞} other {#} } Devices", - "assets-info": "{count} / { max, plural, =0 {∞} other {#} } Assets", - "dashboards-info": "{count} / { max, plural, =0 {∞} other {#} } Dashboards", - "integrations-info": "{count} / { max, plural, =0 {∞} other {#} } Integrations", - "community-support": "Community support", - "white-labeling": "White labeling", - "unlimited-datapoints-and-messages": "Unlimited datapoints and messages", - "unlimited-api-calls": "Unlimited API calls" + "step4": { + "title": "Sukonfigūruokite funkciją: Prekinio ženklo pritaikymas", + "content": "

    Lengvai pritaikykite savo įmonės ar produkto logotipą ir spalvų schemą be programavimo ir paslaugos paleidimo iš naujo.

    Sekite dokumentaciją, kaip tai atlikti:

    " }, - "usage-info": { - "title": "Usage", - "entities": "Entities", - "api-calls": "API calls" + "step5": { + "title": "Sukonfigūruokite funkciją: Dvigubas autentifikavimas (2FA)", + "content": "

    Pagerinkite paskyrų saugumą naudodami dviejų faktorių autentifikavimą.

    Sekite dokumentaciją, kaip tai atlikti:

    " }, - "functions": { - "title": "Functions", - "pe-feature-tooltip": "Only on ThingsBoard\nProfessional Edition", - "switch-to-pe": "Switch to PE", - "alarms": "Alarms", - "dashboards": "Dashboards", - "entities-and-relations": "Entities & Relations", - "profiles": "Profiles", - "advanced-features": "Advanced features", - "notification-center": "Notification center", - "api-usage": "API usage", - "customers": "Customers", - "customers-hierarchy": "Customers hierarchy", - "roles-and-permissions": "Roles & Permissions", - "groups": "Groups", - "integrations": "Integrations", - "solution-templates": "Solution templates", - "scheduler": "Scheduler", - "white-labeling": "White-labeling" + "step6": { + "title": "Sukonfigūruokite funkciją: OAuth 2", + "content": "

    Supaprastinkite nuomininko ir kliento vartotojų prisijungimą naudodami vieningo prisijungimo (Single Sign-On) funkciją per OAuth 2.0.

    Sekite dokumentaciją, kaip tai atlikti:

    " + } + }, + "tenant-admin": { + "step1": { + "title": "Sukurti įrenginį", + "content": "

    Sukurkite savo pirmąjį įrenginį platformoje per naudotojo sąsają (UI). Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-device": "Kaip sukurti įrenginį" }, - "devices": { - "view-docs": "View docs", - "inactive": "Inactive", - "active": "Active", - "total": "Total" + "step2": { + "title": "Prijungti įrenginį", + "content-before": "

    Norint prijungti įrenginį, reikia gauti jo prisijungimo duomenis. Šiame vadove rekomenduojame naudoti numatytuosius automatiškai sugeneruotus prisijungimo duomenis – prieigos raktą (Access Token).

    • Eikite į įrenginių lentelę
    • Spustelėkite įrenginio eilutę, kad atidarytumėte jo informaciją
    • Paspauskite mygtuką „Kopijuoti prieigos raktą“

    Naudokite paprastas HTTP komandas duomenų publikavimui. Nepamirškite pakeisti $ACCESS_TOKEN savo įrenginio prieigos raktu:

    ", + "ubuntu": { + "install-curl": "Įdiekite cURL Ubuntu sistemoje:" + }, + "macos": { + "install-curl": "Įdiekite cURL MacOS sistemoje:" + }, + "windows": { + "install-curl": "Nuo Windows 10 b17063 versijos cURL yra įdiegtas pagal nutylėjimą." + }, + "replace-access-token": "Pakeiskite $ACCESS_TOKEN savo įrenginio prieigos raktu:", + "content-after": "

    Taip pat galite naudoti kitus protokolus, tokius kaip MQTT, CoAP ir kt.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-connect-device": "Kaip prijungti įrenginį" }, - "alarms": { - "critical": "Critical", - "assigned-to-me": "Assigned to me", - "total": "Total" + "step3": { + "title": "Sukurti valdymo skydelį", + "content": "

    Sukurkite valdymo skydelį duomenų vizualizavimui iš objektų, tokių kaip įrenginiai ar ištekliai.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-dashboard": "Kaip sukurti valdymo skydelį" }, - "solution-templates": { - "title": "Solution templates", - "prototype-plan": "Prototype plan", - "startup-plan": "Startup plan" + "step4": { + "title": "Sukonfigūruoti aliarmo taisykles", + "alarm-rules": "Aliarmo taisyklės", + "content": "

    Sukurkime aliarmą, kuris suveiks, kai temperatūra pasieks 25°C. Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-alarm-rules": "Kaip sukonfigūruoti aliarmo taisykles" }, - "getting-started": { - "get-started": "Get started", - "finish": "Finish", - "done-welcome-title": "Welcome on board", - "done-welcome-text": "You did great with it!", - "sys-admin": { - "step1": { - "title": "Create Tenant & Tenant Administrator", - "content": "

    A tenant is an individual or an organization that owns or produces devices and assets. The tenant may have multiple tenant administrator users, customers, devices, and assets.

    The Tenant Administrator can create and manage devices, assets, customers, and dashboards within the tenant account.

    Follow the documentation on how to do it:

    ", - "how-to-create-tenant": "How to create Tenant & Tenant Administrator" - }, - "step2": { - "title": "Configure feature: Mail server", - "content": "

    Mail server configuration is essential for user activation, password recovery, and alarm notification delivery.

    Follow the documentation on how to do it:

    ", - "how-to-configure-mail-server": "How to configure Mail server" - }, - "step3": { - "title": "Configure feature: SMS provider", - "content": "

    Configure SMS providers to notify the customers about the alarms via SMS.

    Follow the documentation on how to do it:

    ", - "how-to-configure-sms-provider": "How to configure SMS provider" - }, - "step4": { - "title": "Configure feature: White-labeling", - "content": "

    Easily customize your company's or product's logo and color scheme without coding and without restarting the service.

    Follow the documentation on how to do it:

    ", - "how-to-configure-white-labeling": "How to configure White-labeling" - }, - "step5": { - "title": "Configure feature: 2FA", - "content": "

    Improve security of the platform accounts with two-factor authentication.

    Follow the documentation on how to do it:

    ", - "how-to-configure-2fa": "How to configure 2FA" - }, - "step6": { - "title": "Configure feature: OAuth 2", - "content": "

    Simplify login for the tenant and customer users with Single Sign-On functionality via OAuth 2.0.

    Follow the documentation on how to do it:

    ", - "how-to-configure-oauth2": "How to configure OAuth 2" - }, - "step7": { - "title": "Configure feature: Slack", - "content": "

    Distribute notifications to the tenant and customer users via Slack according to the notification rules you set.

    Follow the documentation on how to do it:

    ", - "how-to-configure-notifications": "How to configure Slack" - } - }, - "tenant-admin": { - "step1": { - "title": "Create device", - "content": "

    Let's provision your first device to the platform via UI. Follow the documentation on how to do it:

    ", - "how-to-create-device": "How to create Device" - }, - "step2": { - "title": "Connect device", - "content-before": "

    To connect the device you need to get the device credentials. We recommend using default auto-generated credentials which is access token for this guide.

    • Go to device table
    • Click on the device row to open device details
    • Press the button \"Copy access token\"

    Use simple commands to publish data over HTTP. Don't forget to replace $ACCESS_TOKEN with your device access token:

    ", - "ubuntu": { - "install-curl": "Install cURL for Ubuntu:" - }, - "macos": { - "install-curl": "Install cURL for MacOS:" - }, - "windows": { - "install-curl": "Starting Windows 10 b17063, cURL is available by default." - }, - "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", - "content-after": "

    You can also use other protocols such as MQTT, CoAP, etc.

    Follow the documentation on how to do it:

    ", - "how-to-connect-device": "How to connect Device" - }, - "step3": { - "title": "Create dashboard", - "content": "

    Create a dashboard to visualize data from entities such as assets, devices, etc.

    Follow the documentation on how to do it:

    ", - "how-to-create-dashboard": "How to create Dashboard" - }, - "step4": { - "title": "Configure alarm rules", - "alarm-rules": "Alarm rules", - "content": "

    Let's raise an alarm when the temperature reaches 25°C. Follow the documentation on how to do it:

    ", - "how-to-configure-alarm-rules": "How to configure Alarm rules" - }, - "step5": { - "title": "Create alarm", - "content-before": "

    To trigger the alarm, send a new telemetry value of 26°C or higher.

    ", - "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", - "content-after": "

    Follow the documentation on how to do it:

    ", - "how-to-create-alarm": "How to create Alarm" - }, - "step6": { - "title": "Create customer and share dashboard", - "content": "

    By creating end-user dashboards, a customer user can only see his own devices, and data from another customer will be hidden.

    Follow the documentation on how to do it:

    ", - "how-to-create-customer-and-share-dashboard": "How to create Customer and share Dashboard" - } - } - } - }, - "icon": { - "icon": "Icon", - "icons": "Icons", - "select-icon": "Select icon", - "material-icons": "Material icons", - "show-all": "Show all icons", - "search-icon": "Search icon", - "no-icons-found": "No icons found for '{{iconSearch}}'" - }, - "subscription": { - "entity-limit-text": "However you can upgrade your subscription plan in order to increase your limits.", - "upgrade-your-plan": "Upgrade subscription plan", - "white-labeling-feature": "White labeling feature", - "white-labeling-text-full": "Rebrand ThingsBoard platform web interface with your company or product logo and color scheme in 2 minutes.

    Remove “Powered By” on the dashboards footer.
    No coding or service restart required. Allow your customers to white-label their interface as well.", - "enable-white-labeling": "Enable white-labeling feature now by upgrading your subscription plan!", - "read-more": "Read more", - "white-labeling-video-text": "See video tutorial below to see how this feature works!" - }, - "subscription-error": { - "title": "Subscription violation", - "warning-title": "Subscription warning", - "upgrade-subscription-plan": "Please upgrade your subscription plan", - "upgrade-subscription-plan-to-install-solution-template": "In order to install {{solutionTemplateName}} solution you must upgrade your subscription at least to the {{planName}} plan!", - "limit-reached": { - "device-count": "You have reached maximum devices ({{value}}) allowed by your subscription plan!", - "asset-count": "You have reached maximum assets ({{value}}) allowed by your subscription plan!" + "step5": { + "title": "Sukurti aliarmą", + "content-before": "

    Norėdami suaktyvinti aliarmą, siųskite naują telemetrijos reikšmę, didesnę arba lygią 26°C.

    ", + "replace-access-token": "Pakeiskite $ACCESS_TOKEN savo įrenginio prieigos raktu:", + "content-after": "

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-alarm": "Kaip sukurti aliarmą" }, - "feature-disabled": { - "white-labeling": "White Labeling feature is not allowed by your subscription plan!" - } - }, - "phone-input": { - "phone-input-label": "Phone number", - "phone-input-required": "Phone number is required", - "phone-input-validation": "Phone number is invalid or not possible", - "phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}", - "phone-input-hint": "Telefono numeris tarptautiniu formatu , pvz. +37060123456" - }, - "custom": { - "widget-action": { - "action-cell-button": "Action cell button", - "row-click": "On row click", - "polygon-click": "On polygon click", - "marker-click": "On marker click", - "circle-click": "On circle click", - "tooltip-tag-action": "Tooltip tag action", - "node-selected": "On node selected", - "element-click": "On HTML element click", - "pie-slice-click": "On slice click", - "row-double-click": "On row double click", - "card-click": "On card click" + "step6": { + "title": "Sukurti klientą ir pasidalyti valdymo skydeliu", + "content": "

    Sukūrę galutinio naudotojo valdymo skydelius, kliento vartotojas matys tik savo įrenginius, o kitų klientų duomenys bus paslėpti.

    Sekite dokumentaciją, kaip tai atlikti:

    " } - }, - "paginator": { - "items-per-page": "Eilučių skaičius puslapyje:", - "first-page-label": "Pirmas puslapis", - "last-page-label": "Paskutinis puslapis", - "next-page-label": "Kitas puslapis", - "previous-page-label": "Ankstesnis puslapis", - "items-per-page-separator": "iš" + } + } + }, + "icon": { + "icon": "Piktograma", + "icons": "Piktogramos", + "select-icon": "Pasirinkti piktogramą", + "material-icons": "Material piktogramos", + "show-all": "Rodyti visas piktogramas", + "search-icon": "Ieškoti piktogramos", + "no-icons-found": "Nerasta piktogramų pagal '{{iconSearch}}'" + }, + "phone-input": { + "phone-input-label": "Telefono numeris", + "phone-input-required": "Telefono numeris yra privalomas", + "phone-input-validation": "Telefono numeris yra neteisingas arba neįmanomas", + "phone-input-pattern": "Neteisingas telefono numeris. Formatą turi atitikti E.164, pvz. {{phoneNumber}}", + "phone-input-hint": "Telefono numeris tarptautiniu formatu, pvz. +37060123456" + }, + "custom": { + "widget-action": { + "action-cell-button": "Veiksmo langelio mygtukas", + "row-click": "Paspaudus eilutę", + "cell-click": "Paspaudus langelį", + "polygon-click": "Paspaudus daugiakampį", + "marker-click": "Paspaudus žymeklį", + "circle-click": "Paspaudus apskritimą", + "tooltip-tag-action": "Patarimo žymos veiksmas", + "node-selected": "Pasirinkus mazgą", + "element-click": "Paspaudus HTML elementą", + "pie-slice-click": "Paspaudus diagramos dalį", + "row-double-click": "Dukart paspaudus eilutę", + "cell-double-click": "Dukart paspaudus langelį", + "card-click": "Paspaudus kortelę", + "click": "Paspaudus" } + }, + "paginator": { + "items-per-page": "Eilučių skaičius puslapyje:", + "first-page-label": "Pirmas puslapis", + "last-page-label": "Paskutinis puslapis", + "next-page-label": "Kitas puslapis", + "previous-page-label": "Ankstesnis puslapis", + "items-per-page-separator": "iš" + }, + "language": { + "language": "Kalba" + } } - diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 6d49e9f698..62cde47c86 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -692,7 +692,7 @@ pre.tb-highlight { mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header, mat-card-header.mat-mdc-card-header { button.mat-mdc-icon-button { .mat-icon { - color: rgba(0, 0, 0, .54); + color:var(--mat-icon-color, rgba(0, 0, 0, .54)); } &[disabled][disabled] { .mat-icon { @@ -760,11 +760,11 @@ pre.tb-highlight { transition: background-color .2s; &:hover:not(.tb-current-entity) { - background-color: #f4f4f4; + background-color: var(--tb-hover-color, #f4f4f4); } &.tb-current-entity { - background-color: #e9e9e9; + background-color: var(--tb-current-entity-color, #e9e9e9); } &.tb-pointer { @@ -816,7 +816,7 @@ pre.tb-highlight { vertical-align: middle; border-width: 0; border-bottom-width: 1px; - border-bottom-color: rgba(0, 0, 0, 0.12); + border-bottom-color: var(--mat-table-row-item-outline-color,rgba(0, 0, 0, 0.12)); border-style: solid; text-overflow: ellipsis; touch-action: auto !important; @@ -902,6 +902,9 @@ pre.tb-highlight { &.tb-mat-12 { @include tb-mat-icon-size(12); } + &.tb-mat-14 { + @include tb-mat-icon-size(14); + } &.tb-mat-16 { @include tb-mat-icon-size(16); }